Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
461bc23f0a | ||
|
|
cd97612b4d |
@@ -27,3 +27,6 @@ frankenphp
|
|||||||
frankenphp-worker.php
|
frankenphp-worker.php
|
||||||
|
|
||||||
/tests/Browser/Screenshots
|
/tests/Browser/Screenshots
|
||||||
|
|
||||||
|
# Planning notes stay local
|
||||||
|
/docs/plans
|
||||||
|
|||||||
+8
-1
@@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
## [2.0.1] - 2026-09-13
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- The 2.0.0 Docker image did not start: the entrypoint's `php artisan view:cache` failed with "Unable to locate a class or view for component [showcase::example]", because Livewire Material only registered its showcase components while the showcase was enabled, which it is not in production. Livewire Material 1.1.1 registers them always, and a test now caches every view as the entrypoint does.
|
||||||
|
|
||||||
## [2.0.0] - 2026-09-13
|
## [2.0.0] - 2026-09-13
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
@@ -107,5 +113,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
- Dark themed UI built with Livewire, Alpine.js, Tailwind CSS and DaisyUI.
|
- Dark themed UI built with Livewire, Alpine.js, Tailwind CSS and DaisyUI.
|
||||||
- Docker images published to `ghcr.io/surtic86/sealshare`, served by FrankenPHP via Laravel Octane.
|
- Docker images published to `ghcr.io/surtic86/sealshare`, served by FrankenPHP via Laravel Octane.
|
||||||
|
|
||||||
[Unreleased]: https://gitea.nonameweb.ch/noNameWEB/SealShare/compare/v2.0.0...main
|
[Unreleased]: https://gitea.nonameweb.ch/noNameWEB/SealShare/compare/v2.0.1...main
|
||||||
|
[2.0.1]: https://gitea.nonameweb.ch/noNameWEB/SealShare/compare/v2.0.0...v2.0.1
|
||||||
[2.0.0]: https://gitea.nonameweb.ch/noNameWEB/SealShare/releases/tag/v2.0.0
|
[2.0.0]: https://gitea.nonameweb.ch/noNameWEB/SealShare/releases/tag/v2.0.0
|
||||||
|
|||||||
Generated
+3
-3
@@ -2999,11 +2999,11 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "nonameweb/livewire-material",
|
"name": "nonameweb/livewire-material",
|
||||||
"version": "1.1.0",
|
"version": "1.1.1",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://gitea.nonameweb.ch/noNameWEB/livewire-material.git",
|
"url": "https://gitea.nonameweb.ch/noNameWEB/livewire-material.git",
|
||||||
"reference": "d805dbc4076dc06c4d7e5023f99be4c420219975"
|
"reference": "db179e6b4e380bbd6aba2d3766ffcc84e58a8dd2"
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
"laravel/framework": "^13.0",
|
"laravel/framework": "^13.0",
|
||||||
@@ -3083,7 +3083,7 @@
|
|||||||
"material-design",
|
"material-design",
|
||||||
"tailwindcss"
|
"tailwindcss"
|
||||||
],
|
],
|
||||||
"time": "2026-09-13T12:39:26+00:00"
|
"time": "2026-09-13T15:40:33+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "nunomaduro/termwind",
|
"name": "nunomaduro/termwind",
|
||||||
|
|||||||
@@ -1,210 +0,0 @@
|
|||||||
# Colour profiles
|
|
||||||
|
|
||||||
## Goal
|
|
||||||
|
|
||||||
An installation of SealShare can wear one of eight colour profiles instead of the single indigo
|
|
||||||
scheme. The admin picks the profile in Admin settings, previews it on the page while choosing, and
|
|
||||||
on Save it applies to everyone: signed-in users, recipients on the upload and download pages, the
|
|
||||||
Markdown mails and the error pages. Livewire Material learns colour profiles in general — any
|
|
||||||
application lists its own in config, the package generates them, switches between them before the
|
|
||||||
first paint and follows the active one everywhere it draws colour — and ships it as 1.1.0 before
|
|
||||||
SealShare tags 2.0.0.
|
|
||||||
|
|
||||||
## Context
|
|
||||||
|
|
||||||
**Livewire Material 1.0.1** (`../livewire-material`):
|
|
||||||
|
|
||||||
- `php artisan material:scheme {seed} --variant= --contrast= --success= --warning= --info= --output=`
|
|
||||||
(`src/Console/SchemeCommand.php`) runs `resources/node/scheme.mjs` (Google's
|
|
||||||
material-color-utilities, a 93 KB bundle) through Node and writes `resources/css/material-scheme.css`
|
|
||||||
— `:root, [data-theme='light'] { color-scheme: light; --md-sys-color-*: … }` and
|
|
||||||
`[data-theme='dark'] { … }`, about 60 roles each — and `material-scheme.json`
|
|
||||||
(`{seed, variant, spec, contrast, light, dark}`).
|
|
||||||
- Every component and token reads only `--md-sys-color-*` (`resources/css/tokens/theme.css` maps them
|
|
||||||
to Tailwind colours). The package's own default is `resources/css/tokens/scheme.css` and `.json`.
|
|
||||||
- `<x-theme-script>` (in `<head>`, before `@vite`) writes `data-theme`, `data-theme-choice`,
|
|
||||||
`data-theme-key`, `data-rail`, `data-rail-key` on `<html>` before the first paint, and puts them
|
|
||||||
back after a `wire:navigate` swap (`onSwap`). `$store.theme` lives in `resources/js/theme.js`.
|
|
||||||
- `Support\Scheme::load()` / `light()` read `config('livewire-material.scheme')` (the JSON) merged over
|
|
||||||
the package default; the mail theme (`resources/views/mail/theme.blade.php`) and the fallback styles
|
|
||||||
of the error pages (`Support\ErrorPage::fallbackStyles()`) use it.
|
|
||||||
- Tests: `SchemeCommandTest` (runs Node), `TokensTest`, `MailThemeTest`, `ErrorPagesTest`,
|
|
||||||
`ShowcaseTest`, browser tests in three engines; CI on Gitea.
|
|
||||||
|
|
||||||
**SealShare:**
|
|
||||||
|
|
||||||
- One scheme, `#4f46e5` Vibrant (`.ai/rules/css.md` records the exact command).
|
|
||||||
- The production Docker image has no Node, so nothing can be generated at runtime.
|
|
||||||
- Settings → Appearance is the Light/Dark/System picker, stored per browser. All users are admins;
|
|
||||||
recipients are guests.
|
|
||||||
- `App\Livewire\Admin\AdminSettings` holds the settings in `Setting` (key/value) and saves them in
|
|
||||||
`saveSettings()` with one validation call; the form ends in "Save Settings". It uses `Toasts`.
|
|
||||||
- Octane: the application boots once per worker, so anything request-specific must be read per call.
|
|
||||||
|
|
||||||
## Decisions
|
|
||||||
|
|
||||||
- **The admin chooses, nobody else** — one profile for the whole installation; no per-user or
|
|
||||||
per-visitor choice. Light, dark and system stay each visitor's own, as now.
|
|
||||||
- **Ready-made profiles, no free colour** — generated ahead of time with `material:scheme` and shipped
|
|
||||||
in the CSS: correct from the first frame, no generator in the browser, mails and error pages can
|
|
||||||
follow.
|
|
||||||
- **Eight profiles, Vibrant style like today** — `indigo` Indigo `#4f46e5` (the default, today's),
|
|
||||||
`blue` Blue `#0b57d0`, `teal` Teal `#00897b`, `green` Green `#2e7d32`, `amber` Amber `#e8710a`,
|
|
||||||
`rose` Rose `#c2185b`, `violet` Violet `#6750a4`, all `vibrant`; `graphite` Graphite `#5f6368`
|
|
||||||
in the `neutral` style.
|
|
||||||
- **The mechanism is the package's, the profiles are the application's** — Livewire Material gets
|
|
||||||
`profiles` in its config; SealShare lists its eight in its published config. Other applications
|
|
||||||
define their own.
|
|
||||||
- **Swatch picker at the top of Admin settings, previewed live, applied on Save** — a "Colour
|
|
||||||
profile" card with one swatch per profile (primary, secondary and tertiary dots, the name, a check
|
|
||||||
on the chosen one); a click recolours the page at once; "Save Settings" stores it for everyone.
|
|
||||||
Leaving without saving shows the saved profile on the next page.
|
|
||||||
- **Profiles are keyed by `<html data-scheme>`** — the default profile also stands without the
|
|
||||||
attribute, so the stylesheet works before the head script runs and with an unknown name:
|
|
||||||
|
|
||||||
```css
|
|
||||||
:root, [data-theme='light'] { /* default, light */ }
|
|
||||||
[data-theme='dark'] { /* default, dark */ }
|
|
||||||
[data-scheme='teal'], [data-scheme='teal'][data-theme='light'] { /* teal, light */ }
|
|
||||||
[data-scheme='teal'][data-theme='dark'] { /* teal, dark */ }
|
|
||||||
```
|
|
||||||
|
|
||||||
A profile's two-attribute selectors outrank the default's single ones, and its one-attribute
|
|
||||||
selector comes later in the file than `:root`, so the order is part of the format.
|
|
||||||
- **The active profile is resolved on every use, never kept** — the application registers a resolver
|
|
||||||
once (`Scheme::resolveProfileUsing(fn (): ?string => …)`); the head script, the mail theme and the
|
|
||||||
error pages call it each time they draw. A name that is not a generated profile, or no resolver,
|
|
||||||
falls back to the JSON's `default` — the `profile` config (else the first profile) when the scheme
|
|
||||||
was generated. Nothing request-specific is
|
|
||||||
stored on a static, so Octane workers stay clean.
|
|
||||||
- **The JSON keeps its old top-level shape** — `light` and `dark` are still the default profile's
|
|
||||||
roles, beside `default` and `profiles.{name}.{label, seed, variant, spec, contrast, light, dark}`,
|
|
||||||
so a reader of the 1.0 format keeps working.
|
|
||||||
- **`material:scheme` with a seed is unchanged** — one scheme, as in 1.0. Without a seed it generates
|
|
||||||
every configured profile; without either it fails with a message naming both ways.
|
|
||||||
- **A `<x-scheme-picker>` component in the package** — native radios in a `radiogroup`, bound with
|
|
||||||
`wire:model` (or `x-model`), each labelled with the profile's name and its three colours from the
|
|
||||||
JSON; choosing one sets `<html data-scheme>` immediately (the preview). Errors for the bound property
|
|
||||||
show under it.
|
|
||||||
- **The showcase can preview every profile** — a profile menu in its app bar when profiles are
|
|
||||||
configured, recolouring the showcase without storing anything.
|
|
||||||
- **Release** — Livewire Material 1.1.0 (a feature), then SealShare's lock, all before 2.0.0.
|
|
||||||
SealShare's changelog lists it under 2.0.0 "Added".
|
|
||||||
|
|
||||||
## Out of scope
|
|
||||||
|
|
||||||
- A colour picker for any colour, extracting a colour from the logo, or per-profile contrast levels.
|
|
||||||
- Per-user or per-visitor profiles, or a profile switch outside Admin settings.
|
|
||||||
- Changing the website's colours (it stays indigo) or adding profile screenshots.
|
|
||||||
- New success/warning/info sources per profile — they stay the package defaults.
|
|
||||||
|
|
||||||
## Implementation steps
|
|
||||||
|
|
||||||
### Livewire Material 1.1.0 (`../livewire-material`)
|
|
||||||
|
|
||||||
1. **Config.** `config/livewire-material.php`: `'profiles' => []` (name ⇒ `label`, `seed`,
|
|
||||||
`variant`, optional `contrast`) and `'profile' => null` (the fallback name), documented in the
|
|
||||||
config comment beside `scheme`.
|
|
||||||
2. **Generator.** `SchemeCommand`: `seed` becomes optional. Without it, read `profiles`; for each
|
|
||||||
run `scheme.mjs` as today (validating seed, variant and contrast through the generator's own
|
|
||||||
errors), then write the stylesheet in the format under Decisions — the default profile (the
|
|
||||||
`profile` config, else the first) as the plain blocks, then every profile's blocks in config
|
|
||||||
order — and the JSON with `default`, `profiles` and the default's top-level `light`/`dark`. The
|
|
||||||
header comment names the command and says the profiles come from config. With neither a seed nor
|
|
||||||
profiles, fail naming both.
|
|
||||||
3. **Scheme.** `Support\Scheme`: `resolveProfileUsing(?Closure $resolver): void`,
|
|
||||||
`profiles(?string $path = null): array` (name ⇒ label and light/dark roles, from the JSON),
|
|
||||||
`profile(?string $path = null): ?string` (the resolver's answer if it names a profile in the
|
|
||||||
JSON, else `default` from the JSON, else null), and `load(?string $path = null, ?string $profile = null)`
|
|
||||||
returning that profile's roles merged over the package default (the active profile when
|
|
||||||
`$profile` is null; the top-level roles for a 1.0 file). `light()` follows, so the mail theme and
|
|
||||||
`ErrorPage::fallbackStyles()` draw the active profile without further change: the fallback's plain
|
|
||||||
`:root`/`[data-theme]` blocks carry that profile's roles, which is all a page without its build
|
|
||||||
needs. Every method reads the JSON on each call, as `load()` does today.
|
|
||||||
4. **Head script.** `<x-theme-script>`: when the JSON has profiles, write
|
|
||||||
`data-scheme="{active profile}"` on `<html>` with the others, and keep it through `onSwap`.
|
|
||||||
`$store.theme` gains `scheme` (read from the attribute) and `previewScheme(name)` (sets the
|
|
||||||
attribute, stores nothing).
|
|
||||||
5. **Picker.** `resources/views/components/scheme-picker.blade.php` as under Decisions: props
|
|
||||||
`label`, `hint`, `profiles` (default `Scheme::profiles()`), `name`; labels through `__()`. Each
|
|
||||||
swatch is a label around a visually hidden native radio, drawn with Tailwind utilities (a
|
|
||||||
`surface-container` tile, `outline` when checked, a check icon); its three dots are the only inline
|
|
||||||
styles — `background-color` from that profile's light roles, which `Scheme` has already checked
|
|
||||||
are `#rrggbb` — because they show another profile's colours than the page's. `x-on:change` calls
|
|
||||||
`$store.theme.previewScheme($event.target.value)`. With no profiles it renders nothing.
|
|
||||||
6. **Showcase.** A profile menu in `resources/views/showcase/layout.blade.php`'s app bar when profiles
|
|
||||||
exist, calling `previewScheme`; the colour section already reads the variables, so it follows.
|
|
||||||
`src/Showcase/Sections.php` gains the picker as an example (and the search index with it).
|
|
||||||
7. **Docs.** `resources/boost/skills/livewire-material-development/SKILL.md` (Colour scheme: profiles,
|
|
||||||
resolver, picker; the new component in Components), `resources/boost/guidelines/core.blade.php`
|
|
||||||
(one line), `README.md` (Colour scheme and Configuration).
|
|
||||||
8. **Release.** Verify in `.verify` (Feature + Browser in chrome, firefox, safari), push, watch CI,
|
|
||||||
tag `1.1.0`.
|
|
||||||
|
|
||||||
### SealShare
|
|
||||||
|
|
||||||
9. **Package.** `composer update nonameweb/livewire-material` to 1.1.0.
|
|
||||||
10. **Profiles.** `config/livewire-material.php`: the eight profiles under Decisions and
|
|
||||||
`'profile' => 'indigo'`. Run `php artisan material:scheme` to regenerate
|
|
||||||
`resources/css/material-scheme.css` and `.json`; `npm run build`. Update `.ai/rules/css.md`: the
|
|
||||||
scheme is regenerated with `php artisan material:scheme` from the profiles in config, never
|
|
||||||
hand-edited.
|
|
||||||
11. **Resolver.** `AppServiceProvider::boot()`: `Scheme::resolveProfileUsing(fn (): ?string => Setting::get('color_profile'))`.
|
|
||||||
12. **Admin settings.** `AdminSettings`: `public string $colorProfile`, mounted from
|
|
||||||
`Scheme::profile()`; validated with `Rule::in(array_keys(Scheme::profiles()))` in
|
|
||||||
`saveSettings()` — the profiles actually generated into the stylesheet, not merely listed in
|
|
||||||
config; saved with `Setting::set('color_profile', $this->colorProfile)`.
|
|
||||||
`admin-settings.blade.php`: a "Colour profile" card first in the form with
|
|
||||||
`<x-scheme-picker wire:model="colorProfile" :label="__('Colour profile')" />` and a hint that
|
|
||||||
the choice applies to every page, mail and error page after saving.
|
|
||||||
13. **Docs.** README features: "Colour Profiles — eight colour profiles, chosen by the admin".
|
|
||||||
CHANGELOG `2.0.0` "Added". `composer screenshots` again (the admin settings shot shows the new
|
|
||||||
card); the website's feature list gains the same line.
|
|
||||||
|
|
||||||
## Testing
|
|
||||||
|
|
||||||
**Package**
|
|
||||||
|
|
||||||
- `SchemeCommandTest`: with profiles configured and no seed, the JSON has `default` and every profile
|
|
||||||
with light and dark roles, top-level `light`/`dark` equal the default's; the stylesheet has the
|
|
||||||
default's plain blocks first and each profile's `[data-scheme='…']` blocks after, with its hexes;
|
|
||||||
a seed still writes the 1.0 format; neither fails with the message.
|
|
||||||
- `SchemeTest` (new, Feature): `profile()` follows a resolver naming a profile, falls back on an
|
|
||||||
unknown name, on no resolver and on a 1.0 file; `load()` returns the chosen profile's roles.
|
|
||||||
- `MailThemeTest`: the mail's primary is the resolved profile's. `ErrorPagesTest`: the fallback
|
|
||||||
styles carry the resolved profile's roles.
|
|
||||||
- Components: `<x-theme-script>` renders `data-scheme` for the resolved profile and none without
|
|
||||||
profiles; `<x-scheme-picker>` renders a radio per profile, checked from `wire:model`, with the
|
|
||||||
labels.
|
|
||||||
- Browser (three engines): `--md-sys-color-primary` on `<html>` is the profile's in light and in
|
|
||||||
dark, and the default's without the attribute; choosing a swatch changes it at once; the attribute
|
|
||||||
survives `wire:navigate`; the showcase menu previews a profile.
|
|
||||||
|
|
||||||
**SealShare**
|
|
||||||
|
|
||||||
- `AdminSettingsTest`: a valid profile is saved and a toast dispatched; an unknown one fails
|
|
||||||
validation and saves nothing.
|
|
||||||
- `ColourProfileTest` (new, Feature): a guest's upload page renders `data-scheme` from the saved
|
|
||||||
setting and the default without one; the reset-password mail uses the profile's primary.
|
|
||||||
- `tests/Browser/SealShareTest.php`: in Admin settings a swatch recolours the page before saving;
|
|
||||||
after Save and a reload, and on a guest's download page, the profile stays.
|
|
||||||
- `DesignLanguageTest` and `WebsiteTest` keep passing.
|
|
||||||
|
|
||||||
## Risks and open questions
|
|
||||||
|
|
||||||
- **Stylesheet size.** Eight profiles × two themes × ~60 roles is about 60 KB before compression
|
|
||||||
(a few KB gzipped); acceptable, and the CSS stays cacheable.
|
|
||||||
- **A query per page for the setting.** `Setting::get('color_profile')` runs when the head script
|
|
||||||
renders, like the site title already does; cache it later if it ever shows.
|
|
||||||
- **Swatch colours are inline styles.** The design guard does not look at `style` attributes, so
|
|
||||||
nothing stops them spreading; they stay inside `<x-scheme-picker>` and come only from `Scheme`'s
|
|
||||||
checked hexes, which the component test asserts.
|
|
||||||
- **A profile removed from config** while saved leaves the setting pointing nowhere; the resolver's
|
|
||||||
fallback to the default covers it, and Admin settings shows the default as chosen.
|
|
||||||
- **Config and stylesheet out of step.** A profile added to config but not generated is not offered:
|
|
||||||
the picker, the resolver and the validation all read the generated JSON. `.ai/rules/css.md` says to
|
|
||||||
regenerate after changing profiles.
|
|
||||||
- **Open tabs** keep the profile they loaded (or previewed) until their next full load;
|
|
||||||
`wire:navigate` carries the page's current attribute forward.
|
|
||||||
- **Error pages without a build** use the fallback styles, which draw the active profile directly;
|
|
||||||
covered by `ErrorPagesTest`.
|
|
||||||
@@ -1,223 +0,0 @@
|
|||||||
# SealShare on Livewire Material (2.0.0)
|
|
||||||
|
|
||||||
> The package itself — its decisions, the wave plan (Phases 1–10) and its tests — moved to
|
|
||||||
> the package repo on 2026-09-13: [noNameWEB/livewire-material · docs/plans/livewire-material.md](https://gitea.nonameweb.ch/noNameWEB/livewire-material/src/branch/main/docs/plans/livewire-material.md).
|
|
||||||
> This file keeps what SealShare does once the package reaches `1.0.0`.
|
|
||||||
|
|
||||||
## Goal
|
|
||||||
|
|
||||||
SealShare's UI is maryUI 2.9 on daisyUI 5 — a generic web-page look. After this change it runs
|
|
||||||
on **`nonameweb/livewire-material` `^1.0`**: a clean, calm indigo Material 3 Expressive app with
|
|
||||||
a top app bar, light / dark / system theme, and two Expressive moments — the upload drop zone
|
|
||||||
and "link ready" — shipped as SealShare 2.0.0.
|
|
||||||
|
|
||||||
## Context
|
|
||||||
|
|
||||||
**Stacks.** SealShare: Laravel 13.31, Livewire 4.4, maryUI 2.9.10 (no prefix), daisyUI 5.7,
|
|
||||||
Tailwind 4.3, Pest 5.1, Octane on FrankenPHP, PHP 8.5; public on GitHub under MIT, image
|
|
||||||
published to `ghcr.io/surtic86/sealshare`. ReStride: same Laravel / Livewire / Tailwind / Pest,
|
|
||||||
private on `gitea.nonameweb.ch`, CI through Gitea act_runner.
|
|
||||||
|
|
||||||
**SealShare's UI surface** (inventory, 2026-09-13):
|
|
||||||
|
|
||||||
- maryUI tags: `button` 30, `input` 18, `password` 16, `icon` 14, `card` 9 (6 `actions`
|
|
||||||
slots), `menu`/`menu-item` 1/4 (settings nav), `theme-toggle` 3, `toggle` 2, `select` 2,
|
|
||||||
`modal` 2, `table` 1 (`:headers :rows :sort-by with-pagination`, `@scope`), `textarea` 1,
|
|
||||||
`toast` 1 (never triggered).
|
|
||||||
- Raw daisyUI: `btn` (+ `-primary/-ghost/-sm/-xs/-error/-outline/-disabled`), `alert` ×6,
|
|
||||||
`card`/`card-body` (4 admin stat tiles), `join` (2 copy fields), `progress` ×2,
|
|
||||||
`loading` ×2, `badge-success/-error`, `divider`, `link link-primary` ×5, `label`,
|
|
||||||
`file-input`, `checkbox`; tokens `bg-base-*`, `border-base-300`, `text-error/success`,
|
|
||||||
`border-primary(/50)`, `bg-primary/5`; raw `text-green-600`, `bg-white` (QR code).
|
|
||||||
Secondary text is `opacity-50/60/70`.
|
|
||||||
- 20 Heroicons (outline), through `blade-heroicons` pulled in transitively by maryUI.
|
|
||||||
- No `Mary\` PHP coupling. Admin settings flashes `session('message')` into an alert
|
|
||||||
(`AdminSettings.php:116,128,135`). 3 `wire:confirm`.
|
|
||||||
- Layouts: `layouts/app` → `app/sidebar` (centered `max-w-5xl` + footer nav), used by the
|
|
||||||
Livewire pages and all settings SFCs (`config/livewire.php:47`); `layouts/auth` →
|
|
||||||
`auth/simple`. The theme script sits *outside* `<head>` and hard-codes dark, while maryUI's
|
|
||||||
toggle defaults from the OS. `partials/head` loads Instrument Sans from fonts.bunny.net.
|
|
||||||
- Dead: `/dashboard` (starter placeholder, and Fortify's `home`), `welcome`,
|
|
||||||
`pages/auth/register` (still referenced by `Fortify::registerView`,
|
|
||||||
`FortifyServiceProvider.php:52`), `layouts/app/header`, `layouts/auth/{card,split}`,
|
|
||||||
`components/app-logo`, `components/desktop-user-menu`, `components/placeholder-pattern`;
|
|
||||||
the `alpinejs` npm dependency; the Flux credentials step in `tests.yml` and `docker.yml`.
|
|
||||||
- Settings `profile` and `password` show "Saved." through `components/action-message`,
|
|
||||||
listening for `profile-updated` / `password-updated`; `partials/settings-heading` uses a
|
|
||||||
daisyUI `divider`. The 3 `wire:confirm` are admin settings (remove logo, clear system
|
|
||||||
password) and admin dashboard (delete share). `AdminDashboard::headers()` exists only for
|
|
||||||
maryUI's table.
|
|
||||||
- Tests assert text only, never markup; no browser tests.
|
|
||||||
- Docker: the image's caches run in `docker/entrypoint.sh` (`config:cache`, `route:cache`,
|
|
||||||
`view:cache`); `docker/dev-entrypoint.sh` runs `npm run build` against the host's mounted
|
|
||||||
`vendor/` without a `composer install`. The Flux credentials step is in `tests.yml`,
|
|
||||||
`docker.yml` **and** `lint.yml`.
|
|
||||||
- Screens: setup, system password, upload, share created, share download, admin dashboard,
|
|
||||||
admin settings, settings (profile, password, appearance, two-factor), Fortify pages (login,
|
|
||||||
forgot, reset, 2FA challenge, confirm, verify email). Stock Laravel error pages and mails.
|
|
||||||
|
|
||||||
**Constraints found.**
|
|
||||||
|
|
||||||
- SealShare's `Dockerfile` builds assets (stage 1) **before** `composer install` (stage 2);
|
|
||||||
CSS imported from `vendor/` needs the order swapped.
|
|
||||||
- Laravel **replaces** the `errors` view namespace at render time with
|
|
||||||
`config('view.paths')` + `/errors` and the framework's own
|
|
||||||
(`Illuminate/Foundation/Exceptions/RegisterErrorViewPaths.php`), so error views a package
|
|
||||||
adds with `addNamespace('errors', …)` are wiped; only a path in `view.paths` survives.
|
|
||||||
- The package lives at `https://gitea.nonameweb.ch/noNameWEB/livewire-material.git` (public,
|
|
||||||
anonymous reads verified 2026-09-13).
|
|
||||||
|
|
||||||
## Decisions
|
|
||||||
|
|
||||||
The package's decisions are in its own plan. SealShare's:
|
|
||||||
|
|
||||||
- **Converts after `1.0.0`, in one pass, by hand** (~150 tags; no codemod), on branch
|
|
||||||
`material`, released as **2.0.0**.
|
|
||||||
- **Moving SealShare to Gitea is a separate plan** — this plan works wherever it is hosted.
|
|
||||||
- **Seed `#4f46e5` (the favicon's indigo), Vibrant** — chosen after comparing it with Tonal Spot
|
|
||||||
on the upload page in both themes (2026-09-13): Tonal Spot read grey-lavender on this seed.
|
|
||||||
- **Theme default `system`**, storage key `sealshare-theme`, legacy `mary-theme` adopted once.
|
|
||||||
Appearance is a Light / Dark / System connected button group.
|
|
||||||
- **One top app bar everywhere** — logo and site title; a theme toggle for guests, an avatar
|
|
||||||
account menu (Upload, Admin dashboard, Admin settings, Settings, theme, Log out) for users;
|
|
||||||
centered content; Admin and Settings sub-pages as secondary tabs (menu picker on a phone);
|
|
||||||
auth pages a centered card under the same bar. No rail, no bottom bar.
|
|
||||||
- **Expressive components plus two hero moments** — an Expressive shape behind the upload icon
|
|
||||||
that morphs while files are dragged over, the wavy progress indicator for uploads, a
|
|
||||||
shape-backed check when the link is ready; admin stats count up once. Instant under
|
|
||||||
`prefers-reduced-motion`.
|
|
||||||
- **The public download page uses no anchored components** (no menus, no tooltips) — it must
|
|
||||||
work for recipients on iOS below 18.4.
|
|
||||||
- **Starter-kit cleanup during the conversion** — delete the placeholder `/dashboard`, point
|
|
||||||
Fortify `home` at the admin dashboard, delete the unused views and the `registerView`
|
|
||||||
binding, drop `alpinejs` from npm and the Flux step from CI.
|
|
||||||
- **Confirmations become M3 basic dialogs** (the 3 `wire:confirm`) — the browser's native
|
|
||||||
confirm cannot be themed and reads as a different app. *(Not asked in the interview; object
|
|
||||||
in review if you prefer the native confirm.)*
|
|
||||||
- **Save feedback becomes a snackbar** through the package's `Toasts` concern — admin
|
|
||||||
settings' flashed `session('message')` alert and settings' "Saved." `action-message` alike.
|
|
||||||
*(Follows from the snackbar; not asked separately.)*
|
|
||||||
- **The font is self-hosted** — the fonts.bunny.net request goes, which also suits a
|
|
||||||
privacy-minded self-hosted app.
|
|
||||||
- **Tests: updated feature tests, the package's guard as `DesignLanguageTest`, Livewire tests
|
|
||||||
for changed behaviour, and four browser tests** with `pestphp/pest-plugin-browser` (new dev
|
|
||||||
dependency, approved).
|
|
||||||
|
|
||||||
## Out of scope
|
|
||||||
|
|
||||||
- ReStride adopting the package — its own plan, after `1.0.0`.
|
|
||||||
- Moving SealShare's repository, CI and image registry to Gitea — its own plan.
|
|
||||||
- Everything the package plan puts out of scope.
|
|
||||||
- Changes to SealShare's features, routes or information architecture beyond the cleanup above.
|
|
||||||
|
|
||||||
## Implementation steps
|
|
||||||
|
|
||||||
Step numbers continue the original plan's, so references elsewhere stay valid.
|
|
||||||
|
|
||||||
### Phase 11 — SealShare 2.0.0 (after `1.0.0`)
|
|
||||||
|
|
||||||
34. **Branch** `material` from `main`; open the PR so CI runs.
|
|
||||||
35. **Dependencies.** Add the `vcs` repository and `composer require nonameweb/livewire-material:^1.0`;
|
|
||||||
`composer remove robsontenorio/mary` (drops `blade-heroicons` with it);
|
|
||||||
`npm remove daisyui alpinejs`; `composer require --dev pestphp/pest-plugin-browser`.
|
|
||||||
maryUI goes **first** because its class components would shadow the package's same-named
|
|
||||||
anonymous ones; the branch is therefore red from here until step 44, which is accepted —
|
|
||||||
it merges once, green (Decisions: one pass).
|
|
||||||
36. **CI and Docker.** Remove the Flux credentials step from `.github/workflows/tests.yml`,
|
|
||||||
`docker.yml` and `lint.yml`; install Playwright browsers in `tests.yml`. `Dockerfile`: run
|
|
||||||
the Composer stage first and `COPY --from=vendor /app/vendor ./vendor` into the Node stage
|
|
||||||
before `npm run build`. `docker/dev-entrypoint.sh`: run `composer install` when `vendor/` is
|
|
||||||
missing, before `npm run build`. No `icons:cache` anywhere: the package draws its symbols
|
|
||||||
without blade-icons.
|
|
||||||
37. **Styles and scheme.** `resources/css/app.css`: `@import 'tailwindcss'`, the package entry
|
|
||||||
from `vendor/`, `./material-scheme.css`, `@source '../views'` and the package's views; drop
|
|
||||||
the daisyUI plugin, maryUI and pagination `@source`s and the swap safelist.
|
|
||||||
`resources/js/app.js` imports the package JS. Run
|
|
||||||
`php artisan material:scheme "#4f46e5" --variant=vibrant` (chosen over Tonal Spot after
|
|
||||||
comparing both on the upload page in both themes).
|
|
||||||
38. **Head and theme.** `partials/head`: remove fonts.bunny.net; include `<x-theme-script />`
|
|
||||||
before `@vite` (it currently sits outside `<head>`). Publish the config with
|
|
||||||
`theme.default = system`, `storage_key = sealshare-theme`, `legacy_keys = ['mary-theme']`.
|
|
||||||
39. **Layouts.** Rebuild `layouts/app.blade.php` (absorbing `app/sidebar`): `<x-app-bar>` with
|
|
||||||
`app-logo-icon` / branding logo and site title, `<x-theme-toggle>` for guests or
|
|
||||||
`<x-account-menu>` for users (Upload, Admin dashboard, Admin settings, Settings, theme, Log
|
|
||||||
out through `App\Livewire\Actions\Logout`), centered content, `<x-toast>`.
|
|
||||||
`layouts/auth.blade.php` (absorbing `auth/simple`): the same bar and a centered card.
|
|
||||||
40. **Cleanup.** Delete the `/dashboard` route, `dashboard.blade.php`, `placeholder-pattern`,
|
|
||||||
`welcome`, `pages/auth/register` and its `Fortify::registerView` line,
|
|
||||||
`layouts/app/{header,sidebar}`, `layouts/auth/{card,split,simple}`, `app-logo`,
|
|
||||||
`desktop-user-menu`. Fortify `home` → `/admin/dashboard`. Update `AuthenticationTest:22`
|
|
||||||
and `EmailVerificationTest:32,63` to the new redirect; `DashboardTest` is rewritten to
|
|
||||||
assert that a signed-in admin lands on the admin dashboard and `/dashboard` is gone
|
|
||||||
(replacing its placeholder tests, approved in the interview). `RegistrationTest` stays.
|
|
||||||
41. **Public pages.** `livewire/file-uploader`: drop zone with `<x-shape>` behind the upload
|
|
||||||
icon morphing while `dragging`, existing Alpine folder walking and `livewire-upload-*`
|
|
||||||
wiring kept, wavy `<x-progress>`, `<x-loading>` for processing, selected files as
|
|
||||||
`<x-list>`, Share Options `<x-card>` (`<x-toggle>`, `<x-select>`, number `<x-input>`s),
|
|
||||||
`<x-alert>` for storage full, filled primary "Create Share Link".
|
|
||||||
`share-created`: shape-backed check, `<x-input copyable>` for the link, four `<x-stat>`,
|
|
||||||
info `<x-alert>`, "Upload More". `share-download`: password `<x-card>` with
|
|
||||||
`<x-password>`, files as `<x-list>` with download icon buttons, "Download All" — no menus
|
|
||||||
or tooltips. `system-password-prompt`, `setup-wizard` onto fields and buttons.
|
|
||||||
42. **Auth pages** (`login`, `forgot-password`, `reset-password`, `two-factor-challenge`,
|
|
||||||
`confirm-password`, `verify-email`): fields, `<x-checkbox>` for remember me, `link` utility
|
|
||||||
for text links, `auth-session-status` onto `<x-alert>` (drops `text-green-600`).
|
|
||||||
43. **Settings.** `pages/settings/layout` → `<x-section-nav>`; `partials/settings-heading`
|
|
||||||
drops the daisyUI divider for `<x-divider>`; `profile` and `password` show "Saved." as a
|
|
||||||
snackbar through `Toasts` (the `profile-updated` / `password-updated` dispatches stay for
|
|
||||||
any listener) and `components/action-message` is deleted;
|
|
||||||
`appearance` → Light / Dark / System `<x-group>` on `$store.theme` (the only toggle on the
|
|
||||||
page); `two-factor` → `<x-badge>` status, `<x-modal fullscreen>` setup with the QR on a
|
|
||||||
white token surface, `<x-input copyable>` key, recovery codes; `delete-user-form` →
|
|
||||||
`<x-modal>` with a `danger` action.
|
|
||||||
44. **Admin.** `admin-dashboard`: four `<x-stat>` (counting up once), disk usage
|
|
||||||
`<x-progress>`, hand-written `<x-table>` with `<x-sort-header>` and pagination (the
|
|
||||||
`@scope` cells become plain Blade and `AdminDashboard::headers()` goes), view and delete
|
|
||||||
icon buttons, delete confirmation in a basic `<x-modal>` instead of `wire:confirm`.
|
|
||||||
`admin-settings`: cards, `<x-textarea>`, `<x-file>` for the logo with preview,
|
|
||||||
`<x-toggle>`, `<x-select>`, `<x-input suffix>`; "Remove the logo?" and "Remove the system
|
|
||||||
password?" become basic dialogs instead of `wire:confirm`; `AdminSettings` uses `Toasts`
|
|
||||||
instead of `session()->flash('message')` (3 places) and the alert block goes. Keep every
|
|
||||||
existing `data-test` attribute on the element that now plays its role.
|
|
||||||
45. **Error pages and mail.** Confirm the package's error views render in SealShare's theme;
|
|
||||||
set `config/mail.php` `markdown.theme` to `livewire-material::mail.theme`; check the
|
|
||||||
password-reset and verify-email mails.
|
|
||||||
46. **Guards.** `tests/Feature/DesignLanguageTest.php` using `DesignGuard` over
|
|
||||||
`resources/views` and `app/` — no maryUI, no daisyUI, only declared colours, only existing
|
|
||||||
icons. A grep for `base-content|bg-base|btn|mary` returns nothing.
|
|
||||||
47. **Rules and AI.** `php artisan boost:update --discover` to install the package guideline and
|
|
||||||
skill; `record-rule` for SealShare: the scheme is regenerated with `material:scheme`,
|
|
||||||
never hand-edited; the download page stays free of anchored components; the theme key.
|
|
||||||
48. **Docs.** README tech stack and the "Dark Mode" feature line; CHANGELOG `2.0.0`.
|
|
||||||
49. **Ship.** Full suite green on the PR; merge; tag `v2.0.0` (publishes the image through
|
|
||||||
`docker.yml`).
|
|
||||||
|
|
||||||
## Testing
|
|
||||||
|
|
||||||
- Feature tests updated where redirects or text change: `AuthenticationTest`,
|
|
||||||
`EmailVerificationTest`, `DashboardTest` (rewritten), `AdminSettingsTest` (asserts the
|
|
||||||
toast is dispatched instead of the flash), `TwoFactorAuthenticationTest`,
|
|
||||||
`AdminDashboardTest`, `ShareDownloadTest`.
|
|
||||||
- `DesignLanguageTest` through the package guard.
|
|
||||||
- Livewire tests: admin settings save/remove-logo/clear-password dispatch toasts; profile and
|
|
||||||
password updates dispatch the "Saved." toast (`ProfileUpdateTest`, `PasswordUpdateTest`);
|
|
||||||
delete share, remove logo and clear system password go through their dialogs' confirm
|
|
||||||
actions.
|
|
||||||
- Browser tests (`tests/Browser`): upload by drop and by Browse → progress → share created →
|
|
||||||
copy link; the password-protected download page at 393px; admin table sort and delete
|
|
||||||
dialog; a first visit follows the OS theme and Appearance switches it.
|
|
||||||
- Narrow runs per step; the full suite on the PR's CI.
|
|
||||||
|
|
||||||
## Risks and open questions
|
|
||||||
|
|
||||||
- **Scope and time.** The whole catalogue (~45 components plus extras) comes before SealShare
|
|
||||||
changes at all, so its starter-kit bugs (the placeholder `/dashboard`) stay until then.
|
|
||||||
Mitigation: waves tagged `0.x`, each reviewed in the showcase; SealShare keeps working
|
|
||||||
meanwhile.
|
|
||||||
- **Gitea becomes a build dependency.** Every SealShare CI run and Docker build fetches the
|
|
||||||
package from `gitea.nonameweb.ch`; an outage or a sign-in setting reverting breaks builds.
|
|
||||||
Mitigation: dist archives cached by Composer in CI; revisit Packagist if it bites.
|
|
||||||
- **iOS / Safari below 18.4.** Anchored menus and tooltips do not position there. Mitigation:
|
|
||||||
SealShare's download page uses none; native `<select>` stays the fallback everywhere.
|
|
||||||
- **Scheme and spring values are tuned by eye**; Tonal Spot may read washed out on indigo —
|
|
||||||
the Vibrant comparison in step 37 is the check.
|
|
||||||
@@ -1,257 +0,0 @@
|
|||||||
# Screenshots and the SealShare website
|
|
||||||
|
|
||||||
## Goal
|
|
||||||
|
|
||||||
Two things that feed each other. First, one command — `composer screenshots` — produces every
|
|
||||||
screenshot of SealShare from fixed demo data, desktop and phone, light and dark, ready for the web.
|
|
||||||
Second, a static website at **sealshare.nonameweb.ch**, made the way mailifysms.nonameweb.ch is:
|
|
||||||
hand-written HTML and CSS in `website/`, uploaded by hand. The site presents SealShare as what it
|
|
||||||
is — software a company installs to run **its own upload platform**, so it exchanges files with
|
|
||||||
customers securely without relying on an outside service — shows the screenshots, compares
|
|
||||||
SealShare with hosted transfer services and with other self-hosted tools, and tells how to install
|
|
||||||
it. It goes live with 2.0.0. The README gets a few of the same screenshots, and its encryption
|
|
||||||
wording is corrected.
|
|
||||||
|
|
||||||
## Context
|
|
||||||
|
|
||||||
**MailifySMS, the model** (`../MailifySMS`):
|
|
||||||
|
|
||||||
- `website/` holds `index.html`, `privacy_policy.html`, `terms_and_conditions.html`,
|
|
||||||
`css/theme.css` (a palette sampled from the app's screenshots), `css/device-frame.css` (a phone
|
|
||||||
bezel shared with the store canvases), self-hosted Poppins (`fonts/`, OFL) and `img/`
|
|
||||||
(`icon.png`, `hero.jpg`, `screenshots/{light,dark}/NN-name.png` at 540px).
|
|
||||||
- `index.html`: sticky nav with a phone toggle, hero, "How it works", "Key features", a screenshot
|
|
||||||
gallery with a Light/Dark switch (`data-light`/`data-dark` on each `<img>`), FAQ accordion,
|
|
||||||
contact card (`surtic86@gmail.com`), footer (quick links, legal). Plausible:
|
|
||||||
`<script defer data-domain="mailifysms.nonameweb.ch" src="https://plausible.io/js/script.js">`.
|
|
||||||
The page's JS is one inline `<script>` at the end.
|
|
||||||
- `CLAUDE.md` records that `website/` "is a faithful copy of what is deployed, images included, so
|
|
||||||
it can be uploaded wholesale". There is no deploy automation.
|
|
||||||
- `tools/screenshots.sh` (macOS only) drives an emulator and headless Chrome; documented in
|
|
||||||
`CLAUDE.md` § Screenshots, with the reasons behind each quirk.
|
|
||||||
|
|
||||||
**Hosting.** `*.nonameweb.ch` is a wildcard DNS record to `80.74.140.2` (METANET shared hosting,
|
|
||||||
nginx), the same as mailifysms. `sealshare.nonameweb.ch` resolves already; HTTP serves the host's
|
|
||||||
placeholder, HTTPS has no certificate. Creating the site and its Let's Encrypt certificate is done
|
|
||||||
in the hosting panel.
|
|
||||||
|
|
||||||
**SealShare.**
|
|
||||||
|
|
||||||
- Laravel 13.31, Livewire 4.4, Livewire Material 1.0.1, Pest 5.1 with `pestphp/pest-plugin-browser`
|
|
||||||
(Playwright 1.63). The browser tests run the app in-process, so factories, `Storage::fake()` and
|
|
||||||
`travelTo()` shape what the browser sees. `tests/Pest.php` applies `Tests\TestCase` and
|
|
||||||
`RefreshDatabase` to `Feature` and `Browser`, and creates an admin in `beforeEach` (the setup
|
|
||||||
gate).
|
|
||||||
- Device presets: `visit()->on()->macbook14()` is 1512×982 at 2× (a 3024×1964 capture);
|
|
||||||
`on()->iPhone15Pro()` is 393×852 at 3× (1179×2556). `inLightMode()` / `inDarkMode()`,
|
|
||||||
`screenshot(fullPage, filename)`. Screenshots are written to `tests/Browser/Screenshots/<name>.png`;
|
|
||||||
the directory is created, subdirectories in the name are not — names must be flat. Pest empties
|
|
||||||
that directory when a browser run starts, and has no reduced-motion emulation.
|
|
||||||
- Pest's in-process server does not store a multipart upload, so a browser test cannot select files
|
|
||||||
through the file input.
|
|
||||||
- PHP here has GD with WebP and PNG support; the production image does not need it (this is a
|
|
||||||
development tool).
|
|
||||||
- Colours: `resources/css/material-scheme.json` (seed `#4f46e5`, Vibrant) holds the light and dark
|
|
||||||
roles as hexes. Font: Google Sans Flex, `vendor/nonameweb/livewire-material/resources/fonts/google-sans-flex/GoogleSansFlex-Latin.woff2`
|
|
||||||
with its `OFL.txt`. Logo: `resources/views/components/app-logo-icon.blade.php` (SVG).
|
|
||||||
- **Encryption, as the code does it:** `ShareService::createShare()` encrypts each uploaded file on
|
|
||||||
the server with AES-256-GCM (chunked) through `FileEncryptionService`. Without a share password
|
|
||||||
the key is stored in `shares.encryption_key`; with one, the key is derived with PBKDF2-SHA256 and
|
|
||||||
never stored. The server sees the plaintext while uploading and downloading. The README calls
|
|
||||||
this "End-to-End Encryption", which it is not.
|
|
||||||
- The upload page is public, optionally behind the system password (`SystemPasswordGate`); a
|
|
||||||
customer given that password can upload and send the link back.
|
|
||||||
- Install today (README): `ghcr.io/surtic86/sealshare`, clone from GitHub. Gitea
|
|
||||||
(`gitea.nonameweb.ch/noNameWEB/SealShare`) is now the public repository; the image registry for
|
|
||||||
2.0.0 is settled separately.
|
|
||||||
- `.gitignore` does not ignore `tests/Browser/Screenshots`; `.dockerignore` excludes `tests` and
|
|
||||||
`*.md` but would copy a `website/` directory into the image.
|
|
||||||
|
|
||||||
**Peers.** Pingvin Share has been archived since June 2025 (its README points to forks such as
|
|
||||||
Pingvin Share X). PsiTransfer, Gokapi and Erugo are single-purpose self-hosted share tools; Gokapi
|
|
||||||
advertises end-to-end encryption.
|
|
||||||
|
|
||||||
## Decisions
|
|
||||||
|
|
||||||
- **Positioning: software you host, not a service** — the site says plainly that SealShare is not
|
|
||||||
hosted by anyone but the company that installs it: its own upload platform for exchanging files
|
|
||||||
with customers, data on its own server, no dependence on an external service.
|
|
||||||
- **Pages: `index.html` and `privacy.html`** — one landing page, and a short privacy page because
|
|
||||||
the site uses Plausible. No terms page: the software is MIT-licensed and no service is offered.
|
|
||||||
- **A comparison with hosted transfer services and with self-hosted share tools** — two tables on
|
|
||||||
the landing page. Cloud suites (Nextcloud-style) are left out.
|
|
||||||
- **Hand-written HTML and CSS, like MailifySMS** — no build step; `website/` is uploaded as it is.
|
|
||||||
- **Colours copied from `material-scheme.json`, not sampled** — `website/css/theme.css` lists the
|
|
||||||
roles it uses with the scheme's hexes, light by default and dark under
|
|
||||||
`@media (prefers-color-scheme: dark)`; it names the seed and variant it was copied from, so a
|
|
||||||
regenerated scheme is copied again. The site follows the visitor's system theme and has no
|
|
||||||
toggle of its own.
|
|
||||||
- **Google Sans Flex, self-hosted** — the app's font, copied with its `OFL.txt` into
|
|
||||||
`website/fonts/`; nothing is loaded from Google.
|
|
||||||
- **Uploaded by hand, like MailifySMS** — `website/` is a faithful copy of what is live. You create
|
|
||||||
the subdomain and certificate once in the hosting panel and upload the folder when it changes.
|
|
||||||
No hosting credentials anywhere in the repository or CI.
|
|
||||||
- **Plausible** — `data-domain="sealshare.nonameweb.ch"`, the same script as MailifySMS; the site
|
|
||||||
must be added in the Plausible account.
|
|
||||||
- **English only.**
|
|
||||||
- **Screenshots: desktop and phone, each in light and dark (20 images)** —
|
|
||||||
desktop (MacBook 14, 2×): `01-upload` (files selected, options filled), `02-share-created`,
|
|
||||||
`03-qr-code` (the dialog), `04-download` (the recipient's file list), `05-admin-dashboard`,
|
|
||||||
`06-admin-settings`; phone (iPhone 15 Pro, 3×): `01-upload`, `02-password` (the recipient's
|
|
||||||
password prompt), `03-download`, `04-qr-code`.
|
|
||||||
- **Screenshots run as Pest browser tests in `tests/Screenshots/`, started by `composer screenshots`**
|
|
||||||
— the directory is not one of phpunit.xml's test suites, so `php artisan test`, the Browser
|
|
||||||
suite and CI never run it. It reuses Playwright and the in-process server.
|
|
||||||
- **Fixed demo data** — factories and `ShareService` with fixed names, sizes and tokens, time
|
|
||||||
frozen with `travelTo()`, the site title and branding at their defaults, one admin
|
|
||||||
("Alex Morgan"). Every run produces the same images unless the UI changed.
|
|
||||||
- **Images published as WebP by the test run itself** — after each capture a small helper resizes
|
|
||||||
it with GD into `website/img/screenshots/{desktop,phone}/{light,dark}/NN-name-<width>.webp` at two widths
|
|
||||||
(desktop 1600 and 800 px, phone 1080 and 540 px) for `srcset`. Raw PNGs stay in
|
|
||||||
`tests/Browser/Screenshots/`, which is gitignored; only the WebP files are committed.
|
|
||||||
- **Device frames in CSS** — `website/css/device-frame.css` draws a laptop and a phone around the
|
|
||||||
screenshots; the hero shows the desktop upload and the phone download screenshots framed, in
|
|
||||||
the visitor's theme. No generated hero image.
|
|
||||||
- **README shows three screenshots** — desktop upload, desktop share created, phone download (light),
|
|
||||||
referenced from `website/img/screenshots/…`, so each image exists once in the repository.
|
|
||||||
- **Encryption is described accurately, on the site and in the README** — "encrypted at rest with
|
|
||||||
AES-256-GCM; with a share password the key is never stored". The comparison marks end-to-end
|
|
||||||
encryption "no" for SealShare. The README's "End-to-End Encryption" line is corrected.
|
|
||||||
- **Built on `material`, live with 2.0.0** — screenshots show the 2.0.0 interface; the site links
|
|
||||||
the Gitea repository, and its install commands are the README's at release, whatever registry
|
|
||||||
2.0.0 ships with.
|
|
||||||
- **Comparison facts are researched, dated and sourced** — from each product's own site,
|
|
||||||
documentation or repository; the tables say "as of <month year>" and link every source. Hosted
|
|
||||||
services: WeTransfer, SwissTransfer, Dropbox Transfer, Google Drive links. Self-hosted tools:
|
|
||||||
open source, single-purpose, installable with Docker, with a release in the 12 months before the
|
|
||||||
research — expected Pingvin Share X, PsiTransfer, Gokapi and Erugo; any that fails the rule is
|
|
||||||
dropped and named in the commit message. Criteria (rows): where files are stored, who operates
|
|
||||||
it, recipient needs an account, password protection, expiry, download limit, encryption at rest,
|
|
||||||
end-to-end encryption, folder upload, custom branding, maximum file size, licence and cost. A
|
|
||||||
value that cannot be sourced is "—", never guessed.
|
|
||||||
|
|
||||||
## Out of scope
|
|
||||||
|
|
||||||
- A terms page, a documentation section, a German version.
|
|
||||||
- Deploy automation (Gitea Actions, SFTP scripts), and creating the subdomain, certificate or
|
|
||||||
Plausible site — those are yours in the hosting panel and the Plausible account.
|
|
||||||
- Comparing with Nextcloud, ownCloud or other cloud suites.
|
|
||||||
- Store-style canvases with captions, a generated hero image, video or animated screenshots.
|
|
||||||
- Running the screenshots in CI, or checking them against earlier runs (visual regression).
|
|
||||||
- The image registry move and the install commands' final form (settled with 2.0.0).
|
|
||||||
- Implementing end-to-end encryption.
|
|
||||||
|
|
||||||
## Implementation steps
|
|
||||||
|
|
||||||
1. **Housekeeping.** `.gitignore`: `/tests/Browser/Screenshots`. `.dockerignore`: `website`.
|
|
||||||
2. **Screenshot helper.** `tests/Screenshots/Publisher.php` (`Tests\Screenshots\Publisher`):
|
|
||||||
`publish(string $capture, string $device, string $theme, string $name, array $widths): void`
|
|
||||||
reads `tests/Browser/Screenshots/<capture>.png` with GD, and for each width writes
|
|
||||||
`website/img/screenshots/<device>/<theme>/<name>-<width>.webp` (quality 82, aspect kept,
|
|
||||||
directories created). It throws when the capture is missing, so a failed shot fails the run.
|
|
||||||
3. **Demo data.** `tests/Screenshots/DemoData.php`: `admin()`, `shares()` (eight shares with fixed
|
|
||||||
tokens, file names such as `Q3-report.pdf`, `Contract 2026.pdf`, `Product photos/…`, sizes,
|
|
||||||
download counts and expiries, one password-protected, one expired), created through factories
|
|
||||||
and `ShareService` with `Storage::fake('shares')` so the files exist encrypted.
|
|
||||||
4. **Screenshot tests.** `tests/Screenshots/ScreenshotsTest.php`, with `tests/Pest.php` extended to
|
|
||||||
`->in('Feature', 'Browser', 'Screenshots')`:
|
|
||||||
- `beforeEach`: `config(['session.driver' => 'file'])`, `travelTo('2026-10-01 09:30')`, demo
|
|
||||||
data; a `ready()` wait as in `SealShareTest`.
|
|
||||||
- One test per device and theme (four tests), each visiting the pages in turn, waiting for
|
|
||||||
`networkidle` and fonts (`document.fonts.ready`), hiding the text caret, capturing
|
|
||||||
viewport-sized (not full-page) shots, and calling `Publisher::publish()` right after each.
|
|
||||||
- The upload shot with files selected: create Livewire temporary uploads on the fake
|
|
||||||
`livewire-tmp` disk and set the uploader's property through `$wire.$set` with
|
|
||||||
`livewire-file:` references, then fill the options. If Livewire refuses that, the shot shows
|
|
||||||
the drop zone with the options filled instead, and the plan's risk note is updated.
|
|
||||||
- QR dialog: `click('[data-test="show-qr-code"]')`; password prompt: the protected share on the
|
|
||||||
phone; admin pages as the admin.
|
|
||||||
5. **Command.** `composer.json` script `"screenshots"`: `Composer\\Config::disableProcessTimeout`,
|
|
||||||
`npm run build`, `@php vendor/bin/pest tests/Screenshots` — the build first, so the shots show
|
|
||||||
the current assets. Playwright's Chromium must be installed (`npx playwright install chromium`),
|
|
||||||
as for the browser tests.
|
|
||||||
6. **Website scaffold.** `website/`:
|
|
||||||
- `css/theme.css` — tokens copied from `material-scheme.json` (with seed and variant noted),
|
|
||||||
Google Sans Flex `@font-face`, layout, nav, hero, sections, cards, tables (scrolling
|
|
||||||
sideways on a phone), FAQ (`<details>`), footer; light and dark through
|
|
||||||
`prefers-color-scheme`.
|
|
||||||
- `css/device-frame.css` — laptop and phone frames.
|
|
||||||
- `fonts/GoogleSansFlex-Latin.woff2`, `fonts/OFL.txt`; `img/logo.svg` (from `app-logo-icon`),
|
|
||||||
`img/icon.png` (favicon, from `public/`).
|
|
||||||
7. **Landing page.** `website/index.html` (Plausible in `<head>`, one inline script at the end):
|
|
||||||
- nav: Why, Features, Screenshots, Compare, Install, FAQ, Gitea;
|
|
||||||
- hero: "Your own secure upload platform" — self-hosted file exchange with customers, no
|
|
||||||
outside service; buttons "Install" (to #install) and "Source on Gitea"; framed desktop and
|
|
||||||
phone screenshots as `<picture>` elements whose `<source media="(prefers-color-scheme: dark)">`
|
|
||||||
picks the dark captures;
|
|
||||||
- "Why run your own": your server, your domain and branding, customers upload and download
|
|
||||||
without accounts, encrypted at rest, no per-seat pricing;
|
|
||||||
- "How it works": upload → link or QR code → the customer downloads, with expiry, download
|
|
||||||
limit and password;
|
|
||||||
- features (from the README, accurate encryption wording);
|
|
||||||
- screenshots: Desktop/Phone and Light/Dark switches over one gallery (Light/Dark starting on
|
|
||||||
the visitor's system theme), `srcset` for both widths, `loading="lazy"`, descriptive `alt`;
|
|
||||||
- compare: the two dated tables with sources (step 8);
|
|
||||||
- install: the README's Docker quick start and a link to the full instructions on Gitea;
|
|
||||||
- FAQ: "Is it end-to-end encrypted?" (no — at rest, and what a password adds), "Can customers
|
|
||||||
send files to us?" (yes, through the upload page, optionally behind the system password),
|
|
||||||
"How big can files be?" (the README's large-file limits), "What does it cost?" (MIT, your
|
|
||||||
hosting), "Who runs it?" (you);
|
|
||||||
- contact (`surtic86@gmail.com`, as MailifySMS) and footer (Gitea, licence, privacy, noNameWEB).
|
|
||||||
8. **Comparison research.** For each product, record every criterion with its source URL and the
|
|
||||||
date checked; apply the self-hosted selection rule; fill the tables. Keep the notes in the
|
|
||||||
commit message, not in the repository.
|
|
||||||
9. **Privacy page.** `website/privacy.html`: who runs the site (contact), the host (METANET, server
|
|
||||||
logs), Plausible (cookieless, no personal data, EU-hosted, link to its data policy), no other
|
|
||||||
third parties, fonts served locally, contact for questions; dated.
|
|
||||||
10. **README and changelog.** Correct the encryption lines (the intro sentence stays accurate;
|
|
||||||
"End-to-End Encryption" becomes "Encryption at Rest", described as in Decisions), add a
|
|
||||||
Screenshots section with the three images, add the website link. CHANGELOG `2.0.0` "Fixed":
|
|
||||||
the README no longer calls the encryption end-to-end. The website and the screenshot tooling
|
|
||||||
get no changelog entry — they do not change the application.
|
|
||||||
11. **Project notes.** `record-rule` for `website/**`: `website/` is a faithful copy of what is live,
|
|
||||||
uploaded by hand; its colours are copied from `material-scheme.json` and must be copied again
|
|
||||||
when the scheme is regenerated; the comparison is dated and every value sourced. And for
|
|
||||||
`tests/Screenshots/**`: run with `composer screenshots` whenever the interface changes, before
|
|
||||||
a release; the demo data is fixed so runs are reproducible.
|
|
||||||
|
|
||||||
## Testing
|
|
||||||
|
|
||||||
- `tests/Unit/ScreenshotPublisherTest.php`: a generated PNG is written as WebP at each requested
|
|
||||||
width with the aspect ratio kept, into the device/theme directory; a missing capture throws.
|
|
||||||
- `tests/Feature/WebsiteTest.php` guards `website/` without a browser:
|
|
||||||
- every local `src`, `href`, `srcset` entry and CSS `url()` resolves to a file in `website/`;
|
|
||||||
- every screenshot the gallery or README references exists for both widths and both themes;
|
|
||||||
- no request goes to a host other than `plausible.io` (no Google Fonts, no CDN);
|
|
||||||
- `index.html` and `privacy.html` have a `<title>`, `lang="en"` and a meta description;
|
|
||||||
- the README has no "End-to-End Encryption" line, the site's features section does not say
|
|
||||||
"end-to-end", and SealShare's end-to-end cell in the comparison (marked
|
|
||||||
`data-compare="sealshare-e2e"`) reads "No" — the FAQ may still ask the question.
|
|
||||||
- The screenshot run itself is the test of step 4: it fails when a page, selector or capture
|
|
||||||
breaks. It is run by hand before a release, not in CI.
|
|
||||||
- The site is looked at in Chrome, Firefox and Safari, light and dark, at phone width, before
|
|
||||||
uploading.
|
|
||||||
|
|
||||||
## Risks and open questions
|
|
||||||
|
|
||||||
- **Selecting files in the upload shot** works: the files are stored with Livewire's own
|
|
||||||
`FileUploadConfiguration::storeTemporaryFile()` and handed to `_finishUpload` by their signed
|
|
||||||
names; Livewire's temporary-upload cleanup is turned off for the run, because under the frozen
|
|
||||||
clock it deletes them.
|
|
||||||
- **Comparison accuracy and fairness.** Other products change; the tables are dated and sourced,
|
|
||||||
and re-checked when the site is updated. Swiss unfair-competition law expects comparisons to be
|
|
||||||
accurate and not misleading — values that cannot be sourced stay "—".
|
|
||||||
- **Install commands depend on the registry move.** Until it is settled, the install section copies
|
|
||||||
the current README; it is updated before the site goes live with 2.0.0.
|
|
||||||
- **Screenshot determinism.** Relative dates ("in 3 days") depend on `travelTo()`; animations
|
|
||||||
(the share-created shape, counting stats, dialog entry) are waited out — Pest has no
|
|
||||||
reduced-motion emulation — by waiting on `document.getAnimations().length === 0` before each
|
|
||||||
capture.
|
|
||||||
- **The host's name in the privacy page** (METANET) is inferred from the server's reverse DNS
|
|
||||||
(`urbanus.ch-meta.net`); confirm it before the page goes live.
|
|
||||||
- **The contact address** is the one MailifySMS publishes (`surtic86@gmail.com`); change it in
|
|
||||||
step 7 if SealShare should have its own.
|
|
||||||
- **Image weight.** Twenty screenshots at two widths as WebP should stay under ~4 MB in total; if
|
|
||||||
not, lower the quality or drop the larger phone width.
|
|
||||||
- **Colours drift** when the scheme is regenerated; the rule in step 11 and the note in
|
|
||||||
`theme.css` are the guard.
|
|
||||||
@@ -1,149 +0,0 @@
|
|||||||
# Share by QR code and share sheet
|
|
||||||
|
|
||||||
## Goal
|
|
||||||
|
|
||||||
After an upload, the share created page offers two more ways to hand a share over besides
|
|
||||||
copying the link: a QR code, in a dialog, that another device scans (and that downloads as a
|
|
||||||
PNG for chats and mails), and, where the browser has one, the device's native share sheet. Both
|
|
||||||
carry only the share's link — never a password.
|
|
||||||
|
|
||||||
## Context
|
|
||||||
|
|
||||||
- Laravel 13.31, Livewire 4.4, Livewire Material 1.0.x, Pest 5 with browser tests, Octane
|
|
||||||
(FrankenPHP). Production image `dunglas/frankenphp:php8.5-alpine` with `intl`, `pcntl`, `zip`
|
|
||||||
added; it has `xmlwriter` and `iconv`, and neither `gd` nor `imagick`.
|
|
||||||
- `bacon/bacon-qr-code` v3.1.1 is installed through `laravel/fortify` (`^3.0`), which draws the
|
|
||||||
two-factor setup QR with `Writer` + `ImageRenderer` + `SvgImageBackEnd` and strips the XML
|
|
||||||
declaration (`TwoFactorAuthenticatable::twoFactorQrCodeSvg()`). SVG needs no image extension;
|
|
||||||
a server-side PNG would.
|
|
||||||
- `app/Livewire/ShareCreated.php` (`#[Layout('layouts.app')]`, `public Share $share`) renders
|
|
||||||
`resources/views/livewire/share-created.blade.php`: the link as
|
|
||||||
`<x-input :value="route('share.download', $share)" readonly copyable data-test="share-link">`,
|
|
||||||
four `<x-stat>`, an info alert for password-protected shares, and "Upload More". The route
|
|
||||||
`share/{share:token}/created` sits behind `system.password`, like the upload page.
|
|
||||||
- The two-factor dialog (`pages/settings/⚡two-factor`) is the in-app pattern: the SVG inline
|
|
||||||
on a `bg-white` panel inside `<x-modal fullscreen>`, so it stays scannable in dark mode.
|
|
||||||
- `<x-modal>` without `wire:model` opens from `open` in the surrounding Alpine scope and gives
|
|
||||||
`close()`; `materialToast()` is global; `resources/js/app.js` imports only the package JS.
|
|
||||||
- Services live in `app/Services` (`ShareService`, `FileEncryptionService`). Share passwords
|
|
||||||
are hashed and turned into a key; the plain password is never stored.
|
|
||||||
- No feature test covers `ShareCreated` yet; `tests/Browser/SealShareTest.php` copies the link
|
|
||||||
on that page.
|
|
||||||
|
|
||||||
## Decisions
|
|
||||||
|
|
||||||
- **Only on the share created page** — that is where a share is handed over; the admin
|
|
||||||
dashboard and the download page stay as they are.
|
|
||||||
- **A "Show QR code" button opens a dialog** — the page stays as calm as now; the dialog is
|
|
||||||
`<x-modal fullscreen>` (the whole screen on a phone, to hold up to another camera) with the
|
|
||||||
QR on a white panel.
|
|
||||||
- **Download is a PNG made in the browser** — the dialog's SVG is drawn onto a canvas and saved
|
|
||||||
as `share-<token>.png`; no server route and no `gd`/`imagick` in the Docker images.
|
|
||||||
- **Require `bacon/bacon-qr-code:^3.0` directly** — the version already installed through
|
|
||||||
Fortify, declared so SealShare does not depend on Fortify keeping it.
|
|
||||||
- **Password-protected shares get a note in the dialog** — "Recipients also need the password."
|
|
||||||
The QR holds the link only.
|
|
||||||
- **A "Share…" button opens the native share sheet** — shown only where `navigator.share` exists
|
|
||||||
(mostly phones and Safari), sharing `{ title: <site title>, url: <share link> }`.
|
|
||||||
Cancelling the sheet (`AbortError`) does nothing; any other failure shows an error snackbar.
|
|
||||||
- **In 2.0.0, on the `material` branch** — 2.0.0 is not released and the page was just rebuilt
|
|
||||||
there; one PR, one changelog entry.
|
|
||||||
- **Black modules on white, a four-module quiet zone, error correction M** — the most reliable
|
|
||||||
to scan from a screen or a print; the site's theme does not tint it.
|
|
||||||
- **The SVG is drawn at 1024 × 1024** — CSS scales it down in the dialog, and the canvas draws it
|
|
||||||
at its own size, so the PNG is sharp in every browser (Safari rasterises an SVG at its
|
|
||||||
intrinsic size).
|
|
||||||
- **Generated server-side with the page, opened client-side** — the SVG is a few kilobytes and
|
|
||||||
the dialog needs no round trip; the dialog's `open` is Alpine state, not a Livewire property.
|
|
||||||
- **`App\Services\QrCodeService::svg(string $contents): string`** — one place that knows Bacon's
|
|
||||||
API; `ShareCreated::render()` passes `shareUrl`, `qrCodeSvg` and `siteTitle` to the view (as
|
|
||||||
`FileUploader::render()` passes `siteTitle`), so the URL is built once.
|
|
||||||
- **The share and download behaviour lives in `resources/js/share-created.js`** — an
|
|
||||||
`Alpine.data('shareActions', …)` with `canShare`, `share()` and `downloadQrCode()`, imported
|
|
||||||
by `app.js`, instead of long inline Alpine in the view.
|
|
||||||
|
|
||||||
## Out of scope
|
|
||||||
|
|
||||||
- QR codes on the admin dashboard or the download page.
|
|
||||||
- Sharing the QR image itself through the share sheet (`navigator.share({ files })`).
|
|
||||||
- An SVG download, a server-rendered PNG, or a print layout.
|
|
||||||
- A logo in the middle of the QR, or colours from the theme.
|
|
||||||
- Putting the password (or any secret beyond the link's token) into the QR or the share sheet.
|
|
||||||
|
|
||||||
## Implementation steps
|
|
||||||
|
|
||||||
1. **Dependency.** `composer require bacon/bacon-qr-code:^3.0` (stays at v3.1.1).
|
|
||||||
2. **Service.** `php artisan make:class Services/QrCodeService`: `svg(string $contents): string`
|
|
||||||
renders with `new Writer(new ImageRenderer(new RendererStyle(1024, 4, null, null,
|
|
||||||
Fill::uniformColor(new Rgb(255, 255, 255), new Rgb(0, 0, 0))), new SvgImageBackEnd))`,
|
|
||||||
`writeString($contents, Encoder::DEFAULT_BYTE_MODE_ENCODING, ErrorCorrectionLevel::M())`, and
|
|
||||||
drops the XML declaration as Fortify does.
|
|
||||||
3. **Component.** `ShareCreated::render()` builds `$shareUrl = route('share.download',
|
|
||||||
$this->share)` and passes `shareUrl`, `qrCodeSvg` (from the service) and `siteTitle`
|
|
||||||
(`Setting::get('site_title') ?: config('app.name')`) to the view; the link field uses
|
|
||||||
`$shareUrl`.
|
|
||||||
4. **JavaScript.** `resources/js/share-created.js` registers on `alpine:init`
|
|
||||||
`Alpine.data('shareActions', ({ url, title, filename, messages }) => …)`, `messages` holding
|
|
||||||
the translated `shareFailed` and `downloadFailed`:
|
|
||||||
- `open: false` for the dialog;
|
|
||||||
- `canShare`: `typeof navigator.share === 'function'`, read once at init;
|
|
||||||
- `share()`: `navigator.share({ title, url })`, ignoring `AbortError`, otherwise
|
|
||||||
`materialToast(messages.shareFailed, { type: 'error' })`;
|
|
||||||
- `downloadQrCode(svg)`: takes the `<svg>` element (the button passes
|
|
||||||
`$el.closest('dialog').querySelector('[data-qr-code] svg')` — the dialog has its own Alpine
|
|
||||||
scope, so `$refs` from the outer one would not reach it), serialises it, loads it into an
|
|
||||||
`Image` from a Blob URL, draws it on a 1024 × 1024 canvas with a white fill and
|
|
||||||
`imageSmoothingEnabled = false`, `toBlob('image/png')`, clicks a temporary `<a download>`
|
|
||||||
named `filename`, and revokes both object URLs; a failed load or an empty blob shows
|
|
||||||
`materialToast(messages.downloadFailed, { type: 'error' })`.
|
|
||||||
`resources/js/app.js` imports it after the package.
|
|
||||||
5. **View.** In `share-created.blade.php`, wrap the link and actions in
|
|
||||||
`<div x-data="shareActions({ url: @js($shareUrl), title: @js($siteTitle), filename: @js('share-'.$share->token.'.png'), messages: @js(['shareFailed' => __('The share sheet could not open.'), 'downloadFailed' => __('The QR code could not be saved.')]) })">`
|
|
||||||
(a plain element, so `@js` compiles there):
|
|
||||||
- under the link field, a row with `<x-button :label="__('Show QR code')" icon="qr_code_2"
|
|
||||||
variant="tonal" x-on:click="open = true" data-test="show-qr-code" />` and, in a
|
|
||||||
`<span x-show="canShare" x-cloak>` wrapper, `<x-button :label="__('Share…')" icon="share"
|
|
||||||
variant="tonal" x-on:click="share()" data-test="share-sheet" />`;
|
|
||||||
- `<x-modal fullscreen :title="__('Scan to open the share')">` holding
|
|
||||||
`<div data-qr-code class="mx-auto aspect-square w-full max-w-80 rounded-corner-lg bg-white p-2 [&>svg]:size-full">{!! $qrCodeSvg !!}</div>`
|
|
||||||
(the SVG is generated from the app's own URL — no user input), then, for a
|
|
||||||
password-protected share, `<x-alert color="info" icon="lock" :title="__('Recipients also need the password.')" />`,
|
|
||||||
and actions `<x-button :label="__('Download')" icon="download" x-on:click="downloadQrCode($el.closest('dialog').querySelector('[data-qr-code] svg'))" data-test="download-qr-code" />`
|
|
||||||
and `<x-button :label="__('Close')" x-on:click="close()" />`.
|
|
||||||
"Upload More" and the stats stay where they are.
|
|
||||||
6. **Docs.** README: the "Shareable Links" feature line mentions the QR code and share sheet.
|
|
||||||
CHANGELOG `2.0.0`: an "Added" section (before "Changed", as Keep a Changelog orders them)
|
|
||||||
with an entry for both.
|
|
||||||
|
|
||||||
## Testing
|
|
||||||
|
|
||||||
- `tests/Unit/QrCodeServiceTest.php` (the service needs no application):
|
|
||||||
`svg()` returns markup starting with `<svg`, without an XML declaration, 1024 wide, and the
|
|
||||||
same markup for the same contents and different markup for different contents.
|
|
||||||
- `tests/Feature/ShareCreatedTest.php` (new): the page shows the link, and its HTML contains
|
|
||||||
exactly `QrCodeService::svg(route('share.download', $share))` inside the dialog, the
|
|
||||||
"Show QR code" and "Share…" buttons, and the download filename `share-<token>.png`; the
|
|
||||||
password note appears for a protected share and not for an open one.
|
|
||||||
- `tests/Browser/SealShareTest.php`:
|
|
||||||
- "Show QR code" opens the dialog with the QR on a white panel; Download produces an
|
|
||||||
`image/png` blob named `share-<token>.png` (recorded by stubbing
|
|
||||||
`HTMLAnchorElement.prototype.click` through `window.eval`), with no JavaScript errors;
|
|
||||||
- the Share button is hidden where `navigator.share` is missing, and `share()` passes the
|
|
||||||
link to a stubbed `navigator.share`, stays quiet on `AbortError` and shows the error snackbar
|
|
||||||
on any other rejection.
|
|
||||||
- `DesignLanguageTest` keeps passing (`qr_code_2`, `share`, `download` are Material Symbols;
|
|
||||||
`bg-white` is a token).
|
|
||||||
- Narrow runs per step, then the full suite.
|
|
||||||
|
|
||||||
## Risks and open questions
|
|
||||||
|
|
||||||
- **Scanning reliability** is not proven by the tests (no decoder in the stack): the feature
|
|
||||||
test pins the SVG to Bacon's output for the exact URL, and a manual scan with a phone during
|
|
||||||
review is the check.
|
|
||||||
- **The QR is only as right as the link.** Behind a reverse proxy with a wrong `APP_URL` or
|
|
||||||
trusted-proxy setting, both point at the wrong host — unchanged from today.
|
|
||||||
- **Safari and canvas.** Drawing an SVG from a Blob URL onto a canvas works in current
|
|
||||||
Chrome, Firefox and Safari without tainting the canvas; the browser test runs in Chromium
|
|
||||||
locally and in CI, and the three-engine check is manual.
|
|
||||||
- **Share sheet on desktop** exists in Safari and Chromium on some platforms and not in
|
|
||||||
Firefox; the button's absence there is by design.
|
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Support\Facades\File;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* docker/entrypoint.sh caches the configuration, routes and views on every start, with the
|
||||||
|
* Livewire Material showcase off (it only runs locally). A view that does not compile there stops
|
||||||
|
* the container from booting, so the tests compile them as production does.
|
||||||
|
*/
|
||||||
|
|
||||||
|
test('every view compiles as the production entrypoint caches them', function () {
|
||||||
|
expect(config('livewire-material.showcase.enabled'))->toBeFalse();
|
||||||
|
|
||||||
|
$compiled = storage_path('framework/testing/view-cache-'.uniqid());
|
||||||
|
File::ensureDirectoryExists($compiled);
|
||||||
|
config(['view.compiled' => $compiled]);
|
||||||
|
|
||||||
|
try {
|
||||||
|
$this->artisan('view:cache')->assertSuccessful();
|
||||||
|
} finally {
|
||||||
|
File::deleteDirectory($compiled);
|
||||||
|
}
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user