diff --git a/.ai/rules/config.md b/.ai/rules/config.md new file mode 100644 index 0000000..b9a2905 --- /dev/null +++ b/.ai/rules/config.md @@ -0,0 +1,9 @@ +--- +paths: + - config/livewire-material.php +--- + +# Config + +## Theme storage key is sealshare-theme +The theme is stored in localStorage under `sealshare-theme` (default `system`). `mary-theme` stays in `theme.legacy_keys` so visitors from 1.x keep their choice once; don't rename the key or drop the legacy entry, or every returning visitor's theme resets. diff --git a/.ai/rules/css.md b/.ai/rules/css.md new file mode 100644 index 0000000..476d917 --- /dev/null +++ b/.ai/rules/css.md @@ -0,0 +1,9 @@ +--- +paths: + - 'resources/css/material-scheme.*' +--- + +# Css + +## Regenerate the colour scheme, never hand-edit it +material-scheme.css and material-scheme.json are generated together by `php artisan material:scheme "#4f46e5" --variant=vibrant` (Vibrant was chosen over Tonal Spot, which read washed out on the indigo seed). The JSON colours the Markdown mail theme and the fallback error pages, so a hand edit to the CSS alone leaves them out of step. Change the seed or variant and rerun the command instead. diff --git a/.ai/rules/index.md b/.ai/rules/index.md new file mode 100644 index 0000000..999bf21 --- /dev/null +++ b/.ai/rules/index.md @@ -0,0 +1,9 @@ +# Project Rules Index + +Before planning or editing, find the row whose globs match the file's path and read that rule file. + +| Applies to | Rule file | +| --- | --- | +| config/livewire-material.php | .ai/rules/config.md | +| resources/css/material-scheme.* | .ai/rules/css.md | +| resources/views/livewire/share-download.blade.php | .ai/rules/livewire.md | diff --git a/.ai/rules/livewire.md b/.ai/rules/livewire.md new file mode 100644 index 0000000..4178cf4 --- /dev/null +++ b/.ai/rules/livewire.md @@ -0,0 +1,9 @@ +--- +paths: + - resources/views/livewire/share-download.blade.php +--- + +# Livewire + +## No anchored components on the download page +Recipients open share links on any phone, including iOS Safari below 18.4, where CSS anchor positioning is missing. Keep menus, tooltips, selects-as-menus, datepickers and other anchored/popover components off this page; use plain buttons with aria-label and native controls. tests/Browser/SealShareTest.php checks the page at 393px has no popovers. diff --git a/.claude/skills/livewire-material-development/SKILL.md b/.claude/skills/livewire-material-development/SKILL.md new file mode 100644 index 0000000..60f44f2 --- /dev/null +++ b/.claude/skills/livewire-material-development/SKILL.md @@ -0,0 +1,777 @@ +--- +name: livewire-material-development +description: Build Laravel and Livewire views with Livewire Material's Material 3 Expressive Blade components — props, slots, colour roles, type, shape, motion, theming, toasts, the design guard, and the Livewire traps each component handles. +--- + +# Livewire Material Development + +## When to use this skill + +Use this skill when writing or changing any Blade view, Livewire component view or layout in an application that requires `nonameweb/livewire-material`, and when styling, theming or testing such views. + +## Setup + +Composer packages must be installed before the Vite build (in Dockerfiles and CI alike), because the application's build imports from `vendor/`: + +```css +/* resources/css/app.css */ +@import 'tailwindcss'; +@import '../../vendor/nonameweb/livewire-material/resources/css/material.css'; +@import './material-scheme.css'; +@source '../../vendor/nonameweb/livewire-material/resources/views'; +@source '../../vendor/nonameweb/livewire-material/src'; +``` + +```js +// resources/js/app.js +import '../../vendor/nonameweb/livewire-material/resources/js/material.js' +``` + +Every layout puts the theme script in ``, before `@vite`: + +```blade + + + @vite(['resources/css/app.css', 'resources/js/app.js']) + +``` + +## Colour scheme + +The scheme is generated, never hand-edited. Regenerate it with the seed and variant recorded at the top of `resources/css/material-scheme.css`: + +```bash +php artisan material:scheme "#4f46e5" --variant=tonal-spot +``` + +Variants: `tonal-spot` (M3's default), `vibrant`, `expressive`, `neutral`, `fidelity`, `content`, `monochrome`, `rainbow`, `fruit-salad`. `--success`, `--warning` and `--info` set the source of the state colours; `--contrast` goes from -1 to 1. The command also writes `material-scheme.json` beside the stylesheet. + +## Tokens + +Tailwind's default palette is cleared: every colour class names an M3 role. `text-red-600`, `bg-base-200` or `text-gray-500` compile to nothing. + +- Colour roles (`bg-*`, `text-*`, `border-*`, …): `primary`, `on-primary`, `primary-container`, `on-primary-container`, `inverse-primary`, `primary-fixed`, `primary-fixed-dim`, `on-primary-fixed`, `on-primary-fixed-variant`; the same for `secondary` and `tertiary`; `error`, `on-error`, `error-container`, `on-error-container`; `success`, `warning` and `info` with their `on-`, `-container` and `on-…-container`; `inverse-error|success|warning|info`; `surface`, `surface-dim`, `surface-bright`, `surface-container-lowest|low||high|highest`, `on-surface`, `on-surface-variant`, `inverse-surface`, `inverse-on-surface`, `outline`, `outline-variant`, `scrim`, `shadow`; plus `white` and `black`. +- Ink and lines by meaning: `text-body` (body copy), `text-meta` (metadata), `text-quiet` (decoration only), `border-structure`, `border-chrome`, `border-divider` / `divide-divider`. +- Type: `type-{display|headline|title|body|label}-{lg|md|sm}` and `type-emphasized-…`. Never assemble `text-*`, `leading-*` and `tracking-*` by hand. The font is Google Sans Flex (`font-sans`). +- Shape: `rounded-corner-{none|xs|sm|md|lg|lg-increased|xl|xl-increased|xxl|full}`. +- Elevation: `shadow-elevation-{1…5}` — for what floats over content, not for panels (a panel separates by its container tone). +- Motion: `ease-spatial-{fast|default|slow}` (position, size, shape; springs that overshoot) and `ease-effects-{fast|default|slow}` (colour, opacity). Always pair an easing with its duration: `duration-(--md-sys-motion-spatial-fast-duration) ease-spatial-fast`. Reduced motion zeroes the durations. +- States: `state-layer` (M3's hover/focus/press overlay; makes the element `relative` and `isolate`), `focus-ring` (keyboard focus indicator), `link` (a link in running text). +- `dark:` follows the page's theme (`data-theme`), not the operating system. +- `x-figure` on an element holding one number counts it up on first appearance and on change. + +## Theme + +`config/livewire-material.php` → `theme.default` (`light`, `dark` or `system`), `theme.storage_key`, `theme.legacy_keys`. In Alpine, `$store.theme` holds `choice` (what the visitor picked), `resolved` (`light` or `dark`, what shows), `set('light'|'dark'|'system')` and `toggle()`; `x-model="$store.theme.value"` binds a control. + +## Toasts + +```php +use NoNameWeb\LivewireMaterial\Concerns\Toasts; + +class Settings extends Component +{ + use Toasts; + + public function save(): void + { + // … + $this->success('Settings saved'); // also warning(), error(), info() + $this->info('Link copied', timeout: 6000); + $this->success('Share created', redirectTo: route('shares.show', $share)); + } +} +``` + +The methods are protected. They dispatch a `toast` browser event (`assertDispatched('toast', type: 'success', title: 'Settings saved')` in tests). + +## Error pages + +Laravel's HTTP error pages — 403, 404, 419, 429, 500, 503, and the framework's own 401 and 402 — render in M3 without setup. The provider appends the package's error views to `view.paths` after the application's, so a file in `resources/views/errors/` always wins. + +- The pages load `config('livewire-material.showcase.vite')` and ``, so they use the app's scheme, font and theme. While the build is missing (a deploy in progress) they fall back to an inline stylesheet coloured from `resources/css/material-scheme.json`. +- `abort(403, 'Only the owner can open this share.')` and `abort(503, '…')` show the message as the sentence. Every other string goes through `__()`; translate them in `lang/{locale}.json`. +- To change wording or design, run `php artisan vendor:publish --tag=livewire-material-errors`, which copies the layout and pages to `resources/views/errors`. A page extends `errors::minimal` and sets `title`, `code`, `headline`, `message`, `shape` (an `` name) and optionally `actions`: + +```blade +@extends('errors::minimal') + +@section('title', __('Payment Required')) +@section('code', '402') +@section('shape', 'cookie-4') +@section('headline', __('Your plan has ended')) +@section('message', __('Choose a plan to keep using the app.')) + +@section('actions') + +@endsection +``` + +- Maintenance mode: `php artisan down --render="errors::503"`. +- The showcase previews each page at `/material/errors/{code}`. + +## Mail + +Markdown mail (notifications and `markdown:` mailables) wears M3 once the application selects the theme: + +```dotenv +MAIL_MARKDOWN_THEME=livewire-material::mail.theme +``` + +or per mail: `(new MailMessage)->theme('livewire-material::mail.theme')`, or `public $theme = 'livewire-material::mail.theme';` on a mailable. + +- Colours are the light scheme from `resources/css/material-scheme.json` (`livewire-material.scheme`), inlined as hexes; regenerate the scheme and mail follows. Without the file, the package's default scheme applies. +- Write the body as Markdown; the theme styles the bare tags (`#` headings, prose, lists, tables) with M3's typescale. `` is a filled pill in `primary`; `color` also takes `secondary`, `tertiary`, `error`, `success`, `warning` and `info`. `` is a tinted container. +- There is no dark mail. Never put `@media` rules, CSS variables or `color-mix()` in mail CSS: the inliner strips media queries and mail clients resolve no variables. +- The package's mail header (the app name, or a logo) and message (with a replaceable footer) are opt-in: set `MATERIAL_MAIL_COMPONENTS=true`, or `php artisan vendor:publish --tag=livewire-material-mail` to copy them into `resources/views/vendor/mail`. For a logo set `livewire-material.mail.logo` to `['src' => 'https://example.com/logo.png', 'width' => 160, 'height' => 40]`: an absolute URL, with the image at twice those dimensions. +- With those components, a mail can replace the footer: + +```blade + +Your export is ready. + + +© {{ date('Y') }} {{ config('app.name') }} · [Unsubscribe]({{ $unsubscribeUrl }}) + + +``` + +- The showcase renders a sample mail at `/material/mail`. + +## Components + +### `` + +A Material Symbol (Rounded, weight 400, grade 0, 24px), inline. Every symbol on fonts.google.com/icons exists, by Google's name with underscores. An unknown name throws. + +| Prop | Default | | +|---|---|---| +| `name` | required | `calendar_month`, `cloud_upload`, `content_copy` | +| `filled` | `false` | the filled symbol — M3 uses it for active or selected | +| `label` | `null` | names the icon for screen readers when it carries the meaning alone; otherwise it is `aria-hidden` | + +24px (`size-6`) unless a `size-*`, `w-*` or `h-*` class is passed. Colour follows the text: ``. + +### `` + +One of M3 Expressive's 35 shapes, filled in the text colour, `aria-hidden`, sized by its caller: ``. Names: `circle`, `square`, `slanted`, `arch`, `fan`, `arrow`, `semi-circle`, `oval`, `pill`, `triangle`, `diamond`, `clam-shell`, `pentagon`, `gem`, `very-sunny`, `sunny`, `cookie-4`, `cookie-6`, `cookie-7`, `cookie-9`, `cookie-12`, `ghostish`, `clover-4`, `clover-8`, `burst`, `soft-burst`, `boom`, `soft-boom`, `flower`, `puffy`, `puffy-diamond`, `pixel-circle`, `pixel-triangle`, `bun`, `heart`. + +### `` + +The theme decided before the first paint. Exactly once per layout, in ``, before `@vite`. No props; configured in `config/livewire-material.php`. + +### `` + +Label button, icon button, toggle and responsive FAB in one component. + +| Prop | Default | | +|---|---|---| +| `label` / slot | | the words; without them and with an `icon` it is an icon button | +| `variant` | `text` | `filled`, `tonal`, `outlined`, `elevated`, `text` | +| `color` (alias `tone`) | `primary` | `primary`, `secondary`, `tertiary`, `error`, `success`, `warning`, `info` | +| `primary`, `danger`, `caution` | | shorthands: filled primary, filled error, filled warning | +| `size` | `sm` | `xs` 32px, `sm` 40px, `md` 56px, `lg` 96px, `xl` 136px | +| `shape` | `round` | or `square`; both square off further while pressed | +| `icon`, `icon-right` | | Material Symbol names | +| `width` | `default` | icon buttons only: `narrow`, `default`, `wide` | +| `selected` | `null` | `true`/`false` makes it a toggle (`aria-pressed`, selected colours and shape) | +| `link`, `external`, `no-wire-navigate` | | renders ``, with `wire:navigate` unless external | +| `spinner` | | `true` shows the loading indicator while its `wire:click` runs; a string names the action | +| `tooltip`, `tooltip-left`, `tooltip-right`, `tooltip-bottom` | | plain tooltip; also the icon button's accessible name | +| `disabled`, `type`, `responsive`, `fab` | | `responsive` hides the label below `lg`; `fab` is an extended FAB below `sm`, a filled button above | + +```blade + + + +``` + +### `` + +M3's plain tooltip, standalone around any trigger: ``. `side`: `top` (default), `bottom`, `left`, `right`. Shows on hover (fine pointers) and keyboard focus; `aria-hidden`, so the trigger still needs its own accessible name. Buttons and FABs take a `tooltip` prop instead. + +### ``, ``, ``, `` + +```blade + + + + + + + + + + + + +``` + +``: `trigger` slot (its first button or link becomes the menu button), `label`, `position` (`bottom-start` default, `bottom-end`, `top-start`, `top-end`), `vibrant`. ``: `label`, `icon`, `icon-right`, `description`, `shortcut`, `link`, `external`, `selected` (makes it a `menuitemcheckbox`), `disabled`, `keep-open`. Choosing an item closes the menu unless `keep-open`. Keyboard: arrows, Home, End, a letter, Escape (focus returns to the trigger), Tab. + +### `` + +A row of ``s: ``. `connected` sets them 2px apart with small inner corners (a selected toggle rounds fully). Pass the `size` of the buttons inside. + +### `` + +A choice between a few options as a connected button group of native radios (checkboxes with `multiple`): + +```blade + +``` + +Props: `label`, `hint`, `name` (required with `x-model`), `options`, `option-value` (`id`), `option-label` (`name`), `option-icon` (`icon`), `size`, `variant` (`tonal`, `filled`, `outlined`), `multiple`, `inline` (intrinsic width instead of sharing the row). A validation error for the bound property replaces the hint. + +### `` + +```blade + + + +``` + +Attributes go to the leading button; the slot is the menu. `variant` (`filled` default, `tonal`, `outlined`, `elevated`), `color`, `size`, `disabled`, `spinner`, `menu-label`, `position`. + +### `` + +`` — `size` `sm` 56px (default), `md` 80px, `lg` 96px; with `label` it is an extended FAB. `color` `primary`/`secondary`/`tertiary`, drawn in the container, or `variant="filled"`. It does not position itself; wrap it (`
`). `link`, `external`, `disabled`, `type`. + +### ``, `` + +```blade +
+ + + + +
+``` + +Two to six items open above the FAB, which turns into a close button. ``: `icon` (`add`), `label`, `color`, `position` (`top-end` default). Give items the same `color`. Keyboard as ``. + +### `` + +M3 Expressive's loading indicator — a shape morphing through seven Expressive shapes as it turns — for a wait of unknown length. 48px and `primary` unless sized or coloured by class; `contained` sets it on a primary-container circle. A `progressbar` named by `label` ("Loading"); `:label="false"` makes it decorative. It rests under reduced motion. + +```blade + + +
+``` + +`` shows a decorative one in place of its icon. + +### `` + +The snackbar host. Once per layout, near the end of ``: `` (`position="bottom-start"` to leave the centre free). It is `@persist`ed across `wire:navigate` and shows, one at a time, every toast from the `Toasts` concern (see Toasts above) or from JavaScript: + +```js +materialToast('Share deleted', { type: 'success', description: null, timeout: 4000, action: { label: 'Undo', handler: () => $wire.restore() } }) +``` + +`type` (`success`, `error`, `warning`, `info`) adds the state icon; `timeout: 0` keeps it until dismissed; a toast with an action or no timeout gets a close button. Hover or focus pauses the timer. + +### `` + +M3 Expressive's progress indicator: linear (as wide as its container) or `circular` (40px, 48px wavy, unless a `size-*` class is passed), flat or `wavy`, determinate with a `value` or indeterminate without one. + +| Prop | Default | | +|---|---|---| +| `value` | `null` | 0 to `max`, clamped; `null` is indeterminate | +| `max` | `100` | | +| `bind` | `null` | an Alpine expression it follows in the browser; `null`/`undefined` is indeterminate | +| `circular` | `false` | circular instead of linear | +| `wavy` | `false` | Expressive's wave (flat below 10% and from 95%) | +| `thick` | `false` | 8px track and indicator instead of 4px | +| `color` | `primary` | `primary`, `secondary`, `tertiary`, `error`, `success`, `warning`, `info`; the track is the colour's container (secondary-container for primary) | +| `label` | `"Progress"` | names the `progressbar`; `:label="false"` makes it decorative | + +```blade + + + +
+ +
+
+``` + +A value the server changes animates after a morph (the SVG is `wire:ignore`; only the root's attributes change). Under reduced motion values jump, the wave stands still and an indeterminate indicator holds one frame. A `w-*` class narrows a linear one; never pass a display or position class. + +### `` + +- `` — M3's small badge, a dot. `` — M3's large badge, a count. Both `error` by default. `floating` pins it to the top-end corner of a `relative` parent: ``. A dot or count is `aria-hidden` unless it has a `label`; name the control instead ("Messages, 4 unread"). +- ``, ``, `` — a status label (not an M3 badge) in the colour's container or a neutral edge. `color` (alias `tone`): `error` default, `primary`, `secondary`, `tertiary`, `success`, `warning`, `info`. + +### `` + +A notice in the page, in the state's container colour with its icon: + +```blade + + + The upload failed. + + +``` + +`color` (alias `tone`): `info` (default), `success`, `warning`, `error`, `primary`, `secondary`, `tertiary`, `neutral`. `icon` overrides the state icon; `:icon="false"` removes it. Errors and warnings are `role="alert"`, the rest `role="status"`. + +### `` + +A few lines of context around a trigger, with an optional `title` and `actions` slot: + +```blade + + + +``` + +Shows on hover and keyboard focus; `persistent` opens it on press and keeps it until a press elsewhere or Escape (use it when there are actions). `side`: `bottom` (default), `top`, `left`, `right`. + +### `` + +`` — a figure on a surface-container panel; the value counts up on first appearance and when it changes. The slot goes under the description (e.g. a quota's ``). Do not pass a `bg-*` class; wrap it. + +### `` + +"Nothing here yet": `icon` on an Expressive `shape` (`cookie-9` by default), `title`, `description` or slot, and an `actions` slot. Use it for an empty collection, not for a filter that matched nothing. + +### `` + +`variant`: `filled` (default, surface-container-highest), `elevated`, `outlined`; medium corner. Props `title`, `subtitle`, `separator`; slots `figure` (full-bleed media), `menu` (top-end), `actions` (end-aligned). Do not pass `bg-*`; use `variant`. + +A card or list item that opens something is a **row**: `data-list-row` on it and `data-list-open` on its one opener (the title link or a button). A press anywhere else on the row reaches the opener; its other controls keep their own presses. Never wrap a card in `
` or use a stretched link. + +```blade + + {{ $share->name }} + + +``` + +### ``, `` + +``: `label`, `dividers`, `segmented` (M3 Expressive: separate tiles 2px apart). ``: `title` (or slot), `overline`, `description`, leading `icon` / `avatar` (image URL or initials) / `image` / `leading` slot, trailing `trailing` text / `icon-right` / `end` slot, `link` (the whole item becomes a row that opens it), `selected`, `disabled`. One-, two- and three-line heights follow from the content. + +```blade + + @foreach ($files as $file) + + + + @endforeach + +``` + +### `` + +`` — outline-variant line; `vertical`, `inset` (16px start), `middle`, `decorative` (hidden from assistive tech). + +### `` + +A disclosure on native `
`: `` (`variant` `plain` or `filled`; `heading` slot for rich titles). Keeps its state through a morph. + +### `` + +An M3 dialog on native ``. Bind with `wire:model` to a flag or an id; closing (Escape, scrim, `close()`) writes back `false` or `null`. Without `wire:model` it uses `open` from the surrounding Alpine scope. + +```blade + + + + + + +``` + +Props: `title`, `subtitle`, `icon` (centred hero icon), `separator`, `persistent` (no Escape or scrim), `fullscreen` (whole screen below `sm`, for forms), `box-class`. Never remove its `wire:ignore.self` behaviour by re-rendering it conditionally with `@if`; toggle the bound property instead. + +### `` + +An M3 side sheet, bound like ``; `close()` in scope. Props: `title`, `subtitle`, `separator`, `side` (`end` default, `start`), `width` (`25rem`), `with-close-button`, `close-on-escape` (default true), `without-backdrop-close`, `actions` slot. `pane` (with `pane-width`) turns it into a list-detail pane from `xl`: render it after the list inside `
`. Its body is a size container — lay out inside with `@md:` etc., not `sm:`. + +### `` + +An M3 bottom sheet, bound like ``: modal by default (scrim, inert page, drag the handle down or press Escape to close), `standard` for one that is part of the page. Props: `title`, `height` (`90dvh`), `actions` slot. + +### ``, `` + +```blade + + @foreach ($photos as $photo) + + {{ $photo->alt }} + + @endforeach + +``` + +A row of items that change size between M3's keylines as it scrolls (native scroll snap; items are masked, content keeps its size). ``: `layout` (`multi-browse` default, `hero`, `uncontained`, `full-screen`), `item-width` (px or any CSS length; the large size multi-browse aims for, the fixed size uncontained keeps, the cap for hero; 186 by default), `height` (205px), `padding` (px at the ends, 0), `centered` (hero), `label` (the region's name, "Carousel" by default), `controls` (previous/next buttons: default fine pointers only, `true` always, `false` never). ``: slot is an `` (fills and crops) or an element sized `size-full`; `label` overlays a line of text. A focusable `region` of `slide` groups named "n of m"; arrow keys move one item while the row has focus, Home/End to the ends. Works after a Livewire morph, in RTL and under reduced motion. Give items a `wire:key` in a loop. + +### `` + +One component for M3's four chips, picked by `type`: + +| `type` | What it is | Element | +|---|---|---| +| `assist` (default) | an action | `