Compare commits
7
Commits
8f174520eb
...
1.0.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
51bf034b74 | ||
|
|
7ebcb0565f | ||
|
|
889f8a8aa1 | ||
|
|
23218431bf | ||
|
|
eca6b8fb05 | ||
|
|
cbf874a5a6 | ||
|
|
1e580c573e |
@@ -115,3 +115,5 @@ jobs:
|
||||
|
||||
- name: Run browser tests
|
||||
run: vendor/bin/pest --testsuite=Browser --browser ${{ matrix.browser }}
|
||||
env:
|
||||
BROWSER_TIMEOUT: 15000
|
||||
|
||||
@@ -0,0 +1,164 @@
|
||||
# Livewire Material
|
||||
|
||||
Material 3 Expressive components for Laravel and Livewire, built on Tailwind CSS 4.
|
||||
|
||||
- Anonymous Blade components for the current M3 Expressive catalogue: buttons and FABs, menus, chips, text fields, selection controls, sliders, pickers, dialogs and sheets, lists, cards, carousels, progress and loading indicators, snackbars, tabs, app bars, toolbars, navigation bars and rails, an adaptive app shell, data tables and pagination.
|
||||
- A colour scheme generated from one seed colour with Google's colour science (`php artisan material:scheme`), light and dark, and a theme that is chosen before the first paint.
|
||||
- The full Material Symbols Rounded set and the M3 Expressive shapes, drawn inline without an icon package.
|
||||
- Error pages and a Markdown mail theme in the same scheme.
|
||||
- A showcase of every component in the application's own scheme, a design guard for tests, and Laravel Boost guidelines and a skill for AI agents.
|
||||
|
||||
No JavaScript libraries beyond the Alpine that ships with Livewire. Browsers: Chrome 125+, Firefox 147+, Safari 18.4+.
|
||||
|
||||
## Requirements
|
||||
|
||||
PHP 8.4+, Laravel 13, Livewire 4, Tailwind CSS 4 with Vite, and Node (for `material:scheme`).
|
||||
|
||||
## Installation
|
||||
|
||||
The package is served from Gitea. Add the repository and require it:
|
||||
|
||||
```bash
|
||||
composer config repositories.livewire-material vcs https://gitea.nonameweb.ch/noNameWEB/livewire-material.git
|
||||
composer require nonameweb/livewire-material
|
||||
```
|
||||
|
||||
### Stylesheet and script
|
||||
|
||||
The application's build imports from `vendor/`, so Composer packages must be installed before `npm run build` — in a Dockerfile, copy `composer.json`, run `composer install`, then build the assets.
|
||||
|
||||
```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'
|
||||
```
|
||||
|
||||
Do not install Alpine separately; Livewire provides it.
|
||||
|
||||
### Layout
|
||||
|
||||
The theme script goes in `<head>`, before `@vite`, so the page paints in the visitor's theme:
|
||||
|
||||
```blade
|
||||
<!DOCTYPE html>
|
||||
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<x-theme-script />
|
||||
@vite(['resources/css/app.css', 'resources/js/app.js'])
|
||||
</head>
|
||||
<body class="bg-surface font-sans text-on-surface antialiased">
|
||||
{{ $slot }}
|
||||
<x-toast />
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
### Colour scheme
|
||||
|
||||
Generate the scheme from a seed colour. It writes `resources/css/material-scheme.css` (imported above) and `material-scheme.json` (read by the mail theme):
|
||||
|
||||
```bash
|
||||
php artisan material:scheme "#4f46e5" --variant=tonal-spot
|
||||
```
|
||||
|
||||
Variants: `tonal-spot`, `vibrant`, `expressive`, `neutral`, `fidelity`, `content`, `monochrome`, `rainbow`, `fruit-salad`. `--contrast` runs from -1 to 1; `--success`, `--warning` and `--info` seed the state colours. Regenerate instead of editing the file.
|
||||
|
||||
### Configuration
|
||||
|
||||
```bash
|
||||
php artisan vendor:publish --tag=livewire-material-config
|
||||
```
|
||||
|
||||
- `prefix` — components are `<x-button>`, `<x-card>`… Set `'m'` when a name clashes with the application's own components, and they become `<x-m::button>`. `<x-livewire-material::button>` always works.
|
||||
- `theme.default` (`light`, `dark` or `system`), `theme.storage_key`, `theme.legacy_keys` (an earlier toggle's localStorage keys, adopted once).
|
||||
- `fields.variant` — text fields `outlined` (default) or `filled`.
|
||||
- `pagination` — draw Laravel's and Livewire's paginators in M3 (default `true`).
|
||||
- `showcase.enabled`, `showcase.path`, `showcase.middleware`, `showcase.vite`.
|
||||
- `node` — the Node binary for `material:scheme`.
|
||||
|
||||
## Usage
|
||||
|
||||
```blade
|
||||
<x-card title="holiday-photos.zip" subtitle="248 MB · expires in 3 days" variant="outlined">
|
||||
<x-slot:actions>
|
||||
<x-button label="Copy link" icon="content_copy" wire:click="copy" />
|
||||
<x-button label="Delete" danger wire:click="$set('confirming', true)" />
|
||||
</x-slot:actions>
|
||||
</x-card>
|
||||
|
||||
<x-modal wire:model="confirming" title="Delete this share?" icon="delete">
|
||||
Recipients lose access at once.
|
||||
<x-slot:actions>
|
||||
<x-button label="Cancel" x-on:click="close()" />
|
||||
<x-button label="Delete" danger wire:click="delete" />
|
||||
</x-slot:actions>
|
||||
</x-modal>
|
||||
```
|
||||
|
||||
```php
|
||||
use NoNameWeb\LivewireMaterial\Concerns\Toasts;
|
||||
|
||||
class Shares extends Component
|
||||
{
|
||||
use Toasts;
|
||||
|
||||
public function copy(): void
|
||||
{
|
||||
$this->success('Link copied');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Every component, prop and slot is documented in the Boost skill (`resources/boost/skills/livewire-material-development/SKILL.md`) and shown in the showcase.
|
||||
|
||||
## Showcase
|
||||
|
||||
While the application runs locally (or with `MATERIAL_SHOWCASE=true`), `/material` shows every token and component, in every variant, in the application's own scheme and theme: an overview, and a page per section behind a navigation rail (the package's own app shell), with a search over every section, example and component (press `/`).
|
||||
|
||||
## Testing the design
|
||||
|
||||
```php
|
||||
use NoNameWeb\LivewireMaterial\Testing\DesignGuard;
|
||||
|
||||
it('uses only what compiles', function () {
|
||||
expect(DesignGuard::scan([resource_path('views'), resource_path('js'), app_path()])
|
||||
->forbidColours(['tertiary'])
|
||||
->violations())->toBe([]);
|
||||
});
|
||||
```
|
||||
|
||||
The guard fails on maryUI tags, daisyUI classes, colours the theme does not declare, unknown Material Symbol names and Blade directives written inside component tags.
|
||||
|
||||
## AI agents
|
||||
|
||||
With [Laravel Boost](https://github.com/laravel/boost), `php artisan boost:install` (or `boost:update --discover`) picks up the package's guideline and the `livewire-material-development` skill.
|
||||
|
||||
## Developing the package
|
||||
|
||||
```bash
|
||||
composer install && npm install
|
||||
npm run build # the Workbench's assets (or `npm run dev` while working)
|
||||
composer serve # the showcase at http://127.0.0.1:8000/material
|
||||
vendor/bin/pest --testsuite=Feature
|
||||
npx playwright install
|
||||
vendor/bin/pest --testsuite=Browser --browser chrome # also firefox, safari
|
||||
```
|
||||
|
||||
## Credits
|
||||
|
||||
Material Symbols, the M3 Expressive shapes, Google Sans Flex, material-color-utilities and Jetpack Compose Material 3's tokens and algorithms are Google's and the Android Open Source Project's; see `NOTICE`.
|
||||
|
||||
## License
|
||||
|
||||
MIT. See `LICENSE`.
|
||||
@@ -8,8 +8,10 @@ return [
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Every component is an anonymous Blade component. Without a prefix they are
|
||||
| <x-button>, <x-card> and so on; set a prefix such as 'm-' when a name
|
||||
| clashes with one of the application's own components (<x-m-button>).
|
||||
| <x-button>, <x-card> and so on; set a prefix such as 'm' when a name
|
||||
| clashes with one of the application's own components, and they become
|
||||
| <x-m::button>, <x-m::card>. They are always <x-livewire-material::button>
|
||||
| as well.
|
||||
|
|
||||
*/
|
||||
|
||||
|
||||
@@ -578,6 +578,34 @@ pages and the mail theme by an agent in its own worktree. What changed from the
|
||||
(drift test green); README (install, CSS/JS wiring, scheme, theme, prefix, showcase,
|
||||
guard, Docker ordering note); tag `1.0.0`.
|
||||
|
||||
**Phase 10 is done (2026-09-13).** What changed from the step above:
|
||||
|
||||
- **The package's own views write `<x-livewire-material::name>`** for the components they use. Blade
|
||||
spells a configured prefix `<x-m::button>` (not `<x-m-button>`, as the config first claimed), and
|
||||
an unqualified `<x-button>` inside a package view would have broken under a prefix and been
|
||||
shadowed by an application's own `components/button.blade.php`. A test scans every package view
|
||||
(outside comments and the showcase's example heredocs) for unqualified tags, and the showcase
|
||||
rewrites its examples to the configured prefix. The component path string stays
|
||||
`__DIR__.'/../resources/views/components'`: Blade names the view namespace after its hash, and
|
||||
compiled views keep that name.
|
||||
- **A test checks that every component appears in the showcase.**
|
||||
- **The showcase is the package's own app shell**: an overview at `/material` and a page per section
|
||||
(`/material/{section}`, `Showcase\Sections`), grouped in the navigation rail, moved between with
|
||||
wire:navigate. It turned up a WebKit bug in `<x-menu>`: inside a focusable region (the shell's
|
||||
`<main tabindex="-1">`), WebKit hands focus back to that region as the popover closes, so the
|
||||
menu now reads whether focus was inside on `beforetoggle` before returning it to the trigger.
|
||||
- **The showcase has a search** in a search app bar (`<x-search>`, `/` or Ctrl+K): an index of every
|
||||
section, every example (by its anchor) and every component, read from the section views and fetched
|
||||
as `/material/search.json` on first focus. Names in a slot's Alpine scope must keep clear of the
|
||||
component's own: `results` and `open` in `<x-search>`'s scope hid the page's. wire:navigate keeps a
|
||||
URL's hash but scrolls to the top, so the layout lands on the hash after the swap settles.
|
||||
- **Verified in a fresh Laravel 13.31 application** (`composer create-project`, the package from a
|
||||
path repository, the README's CSS, JS and layout, `material:scheme "#4f46e5"`, `npm run build`):
|
||||
a Livewire page with an app bar, tabs, a card, a form with validation, a date picker, a dialog and
|
||||
a toast works without console errors; `/material` and the 404 page render in its scheme.
|
||||
- **`<x-datepicker clearable>`** empties a date, or both ends of a range, as `<x-timepicker clearable>`
|
||||
does.
|
||||
|
||||
### Phase 11 — SealShare 2.0.0
|
||||
|
||||
Tracked in SealShare's `docs/plans/livewire-material.md`, after `1.0.0`.
|
||||
|
||||
@@ -523,7 +523,7 @@ M3 date pickers on a text field. `wire:model` stores `Y-m-d` strings (`x-model`
|
||||
```blade
|
||||
<x-datepicker label="Expires on" wire:model.live="expiresOn" :min="now()" :max="now()->addMonth()" />
|
||||
<x-datepicker label="Birthday" mode="modal" wire:model="birthday" :max="now()" />
|
||||
<x-datepicker label="Trip" range wire:model="trip" hint="Start and end" />
|
||||
<x-datepicker label="Trip" range wire:model="trip" hint="Start and end" clearable />
|
||||
```
|
||||
|
||||
| Prop | Default | |
|
||||
@@ -534,6 +534,7 @@ M3 date pickers on a text field. `wire:model` stores `Y-m-d` strings (`x-model`
|
||||
| `label`, `hint`, `icon`, `variant`, `size` | | the field's |
|
||||
| `value` | `null` | the initial value without `wire:model` |
|
||||
| `name` | | adds hidden inputs with `Y-m-d` for a plain form post (`name[start]`, `name[end]` for a range) |
|
||||
| `clearable` | `false` | a button that empties the field (both ends of a range) once it holds a date |
|
||||
|
||||
Picking in the calendar is a draft; OK or Enter on a day keeps it, Cancel or Escape does not. A typed date is the value once it is whole and allowed; otherwise the field says why. Month and weekday names, the week's first day and the typed format follow `app()->getLocale()`. Keyboard: arrows, Home/End (week), PageUp/PageDown (month; with Shift, year), Space, Enter, Escape. `min` and `max` are read when the picker starts: when they change on the server, give the component a `wire:key` that changes with them. `required`, `disabled` and `readonly` reach the text field.
|
||||
|
||||
@@ -764,7 +765,7 @@ It fails on maryUI tags, daisyUI classes, colours the theme does not declare, un
|
||||
|
||||
## Conventions
|
||||
|
||||
- Components are anonymous Blade components: `<x-name>` without a prefix, or `<x-{prefix}name>` when `config('livewire-material.prefix')` is set.
|
||||
- Components are anonymous Blade components: `<x-name>` without a prefix, or `<x-{prefix}::name>` when `config('livewire-material.prefix')` is set; `<x-livewire-material::name>` always works.
|
||||
- Write class names out whole. Tailwind cannot compile `'text-'.$tone` or `type-{{ $size }}`, and the design guard cannot read them.
|
||||
- The showcase at `/material` (local only, `MATERIAL_SHOWCASE=true` to force it) renders every token and component.
|
||||
|
||||
|
||||
@@ -69,6 +69,9 @@ const MEDIUM_ITEM_FLEX_PERCENTAGE = 0.1
|
||||
/** A programmatic scroll still counts as where the carousel is going for this long. */
|
||||
const TARGET_MS = 700
|
||||
|
||||
// How long the row must go without a scroll event to count as having come to rest.
|
||||
const SETTLE_MS = 150
|
||||
|
||||
const ITEM = '[data-material-carousel-item]'
|
||||
const INTERACTIVE = 'a[href], button, input, select, textarea, summary, [contenteditable], [tabindex]:not([tabindex="-1"])'
|
||||
|
||||
@@ -805,6 +808,7 @@ document.addEventListener('alpine:init', () => {
|
||||
frame: null,
|
||||
target: null,
|
||||
targetAt: 0,
|
||||
settle: null,
|
||||
listeners: [],
|
||||
mutations: null,
|
||||
resizes: null,
|
||||
@@ -817,7 +821,12 @@ document.addEventListener('alpine:init', () => {
|
||||
|
||||
state.reducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)')
|
||||
|
||||
this.listen(scroller, 'scroll', () => this.schedule(), { passive: true })
|
||||
this.listen(scroller, 'scroll', () => this.scrolled(), { passive: true })
|
||||
|
||||
// A scroll the person makes themselves is theirs to end wherever it ends.
|
||||
for (const type of ['pointerdown', 'wheel', 'touchstart']) {
|
||||
this.listen(scroller, type, () => (state.target = null), { passive: true })
|
||||
}
|
||||
this.listen(scroller, 'keydown', (event) => this.navigate(event))
|
||||
this.listen(scroller, 'focusin', (event) => this.reveal(event))
|
||||
this.listen(scroller, 'click', (event) => this.open(event))
|
||||
@@ -913,6 +922,33 @@ document.addEventListener('alpine:init', () => {
|
||||
state.frame ??= requestAnimationFrame(() => this.render())
|
||||
},
|
||||
|
||||
/**
|
||||
* Each scroll frame, and once the row comes to rest: a scroll the buttons or keys started
|
||||
* must end on its item. WebKit on Linux can re-snap a smooth scroll to the item it left
|
||||
* when the masks change the layout under it, so an arrival somewhere else is sent on
|
||||
* again, once, at once.
|
||||
*/
|
||||
scrolled() {
|
||||
this.schedule()
|
||||
|
||||
clearTimeout(state.settle)
|
||||
state.settle = setTimeout(() => {
|
||||
const target = state.target
|
||||
|
||||
if (target === null || performance.now() - state.targetAt > TARGET_MS * 3) {
|
||||
return
|
||||
}
|
||||
|
||||
state.target = null
|
||||
|
||||
const left = state.snaps[target]
|
||||
|
||||
if (left !== undefined && Math.abs(this.scrollOffset() - left) > 1) {
|
||||
this.$refs.scroller.scrollTo({ left: state.rtl ? -left : left, behavior: 'instant' })
|
||||
}
|
||||
}, SETTLE_MS)
|
||||
},
|
||||
|
||||
/** Carousel.kt's carouselItem layer block, for every item. */
|
||||
render() {
|
||||
cancelAnimationFrame(state.frame)
|
||||
@@ -1076,6 +1112,7 @@ document.addEventListener('alpine:init', () => {
|
||||
|
||||
destroy() {
|
||||
cancelAnimationFrame(state.frame)
|
||||
clearTimeout(state.settle)
|
||||
state.listeners.forEach((remove) => remove())
|
||||
state.resizes?.disconnect()
|
||||
state.mutations?.disconnect()
|
||||
|
||||
@@ -366,6 +366,18 @@ document.addEventListener('alpine:init', () => {
|
||||
}
|
||||
},
|
||||
|
||||
/** The clear button: no date (no start and no end), closed, and focus back in the field. */
|
||||
clear() {
|
||||
if (this.open) {
|
||||
this.cancel(false)
|
||||
}
|
||||
|
||||
this.fieldError = ''
|
||||
this.text = ''
|
||||
this.write(null)
|
||||
this.$refs.input.focus()
|
||||
},
|
||||
|
||||
fieldProblem(value) {
|
||||
if (!config.range) {
|
||||
return this.problem(value)
|
||||
|
||||
+11
-1
@@ -15,6 +15,7 @@ document.addEventListener('alpine:init', () => {
|
||||
window.Alpine.data('materialMenu', () => ({
|
||||
closedAt: -Infinity,
|
||||
returnFocus: true,
|
||||
focusWasInside: false,
|
||||
listeners: [],
|
||||
|
||||
init() {
|
||||
@@ -26,6 +27,13 @@ document.addEventListener('alpine:init', () => {
|
||||
// and close() have already done their part, synchronously, because this event is
|
||||
// queued and a screen reader or a test reading aria-expanded in between would be told
|
||||
// the menu is shut.
|
||||
// Whether focus was in the menu is read before it closes: once closed, a browser may
|
||||
// already have handed focus to what had it before the menu opened (WebKit does, when
|
||||
// that was a focusable region around the trigger).
|
||||
this.listen(menu, 'beforetoggle', (event) => {
|
||||
this.focusWasInside = event.newState === 'closed' && menu.contains(document.activeElement)
|
||||
})
|
||||
|
||||
this.listen(menu, 'toggle', (event) => {
|
||||
const opened = event.newState === 'open'
|
||||
|
||||
@@ -37,9 +45,11 @@ document.addEventListener('alpine:init', () => {
|
||||
|
||||
this.closedAt = performance.now()
|
||||
|
||||
if (this.returnFocus && menu.contains(document.activeElement)) {
|
||||
if (this.returnFocus && (this.focusWasInside || menu.contains(document.activeElement))) {
|
||||
this.control()?.focus()
|
||||
}
|
||||
|
||||
this.focusWasInside = false
|
||||
})
|
||||
|
||||
// A press outside closes the menu without pulling focus back to the trigger.
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
$initials = $image ? null : ($avatar ?? \Illuminate\Support\Str::of((string) $name)->explode(' ')->filter()->take(2)->map(fn (string $word): string => mb_strtoupper(mb_substr($word, 0, 1)))->join(''));
|
||||
@endphp
|
||||
|
||||
<x-menu :$label :$position {{ $attributes }}>
|
||||
<x-livewire-material::menu :$label :$position {{ $attributes }}>
|
||||
<x-slot:trigger>
|
||||
<button
|
||||
type="button"
|
||||
@@ -45,7 +45,7 @@
|
||||
@elseif (filled($initials))
|
||||
{{ $initials }}
|
||||
@else
|
||||
<x-icon name="person" />
|
||||
<x-livewire-material::icon name="person" />
|
||||
@endif
|
||||
</button>
|
||||
</x-slot:trigger>
|
||||
@@ -60,20 +60,20 @@
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<x-menu-separator />
|
||||
<x-livewire-material::menu-separator />
|
||||
@endif
|
||||
|
||||
{{ $slot }}
|
||||
|
||||
@if ($theme)
|
||||
<x-menu-item icon="contrast" x-on:click="$store.theme.toggle()" data-account-theme>
|
||||
<x-livewire-material::menu-item icon="contrast" x-on:click="$store.theme.toggle()" data-account-theme>
|
||||
<span x-text="$store.theme.resolved === 'dark' ? @js(__('Light theme')) : @js(__('Dark theme'))">{{ __('Theme') }}</span>
|
||||
</x-menu-item>
|
||||
</x-livewire-material::menu-item>
|
||||
@endif
|
||||
|
||||
@isset($footer)
|
||||
<x-menu-separator />
|
||||
<x-livewire-material::menu-separator />
|
||||
|
||||
{{ $footer }}
|
||||
@endisset
|
||||
</x-menu>
|
||||
</x-livewire-material::menu>
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
{{ $attributes->class(['flex items-start gap-3 rounded-corner-md p-4', $colours]) }}
|
||||
>
|
||||
@if ($icon)
|
||||
<x-icon :name="$icon" filled class="size-6" />
|
||||
<x-livewire-material::icon :name="$icon" filled class="size-6" />
|
||||
@endif
|
||||
|
||||
<div class="min-w-0 flex-1 space-y-1 self-center">
|
||||
@@ -59,7 +59,7 @@
|
||||
|
||||
@if ($dismissible)
|
||||
<button type="button" class="state-layer focus-ring -m-2 inline-flex size-10 shrink-0 items-center justify-center rounded-corner-full" aria-label="{{ __('Dismiss') }}" x-on:click="shown = false">
|
||||
<x-icon name="close" class="size-5" />
|
||||
<x-livewire-material::icon name="close" class="size-5" />
|
||||
</button>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@@ -88,7 +88,7 @@
|
||||
class="sr-only focus:not-sr-only focus:fixed focus:start-4 focus:top-[calc(env(safe-area-inset-top)+1rem)] focus:z-[60] focus:rounded-corner-full focus:bg-inverse-surface focus:px-4 focus:py-2 focus:type-label-lg focus:text-inverse-on-surface focus:shadow-elevation-3 focus:outline-none"
|
||||
>{{ __('Skip to content') }}</a>
|
||||
|
||||
<x-navigation-rail mode="adaptive" :label="$label" :width="$railWidth">
|
||||
<x-livewire-material::navigation-rail mode="adaptive" :label="$label" :width="$railWidth">
|
||||
@isset($brand)
|
||||
<x-slot:brand>{{ $brand }}</x-slot:brand>
|
||||
@endisset
|
||||
@@ -99,14 +99,14 @@
|
||||
|
||||
@foreach ($groups as $group)
|
||||
@if ($group->first()['section'] !== null)
|
||||
<x-navigation-rail-section :label="$group->first()['section']">
|
||||
<x-livewire-material::navigation-rail-section :label="$group->first()['section']">
|
||||
@foreach ($group as $item)
|
||||
<x-navigation-rail-item :label="$item['title']" :icon="$item['icon']" :link="$item['url']" :active="$item['active']" :badge="$item['badge']" :no-wire-navigate="! $item['navigate']" />
|
||||
<x-livewire-material::navigation-rail-item :label="$item['title']" :icon="$item['icon']" :link="$item['url']" :active="$item['active']" :badge="$item['badge']" :no-wire-navigate="! $item['navigate']" />
|
||||
@endforeach
|
||||
</x-navigation-rail-section>
|
||||
</x-livewire-material::navigation-rail-section>
|
||||
@else
|
||||
@foreach ($group as $item)
|
||||
<x-navigation-rail-item :label="$item['title']" :icon="$item['icon']" :link="$item['url']" :active="$item['active']" :badge="$item['badge']" :no-wire-navigate="! $item['navigate']" />
|
||||
<x-livewire-material::navigation-rail-item :label="$item['title']" :icon="$item['icon']" :link="$item['url']" :active="$item['active']" :badge="$item['badge']" :no-wire-navigate="! $item['navigate']" />
|
||||
@endforeach
|
||||
@endif
|
||||
@endforeach
|
||||
@@ -122,7 +122,7 @@
|
||||
@endisset
|
||||
</x-slot:footer>
|
||||
@endif
|
||||
</x-navigation-rail>
|
||||
</x-livewire-material::navigation-rail>
|
||||
|
||||
<div class="flex min-w-0 flex-1 flex-col">
|
||||
{{ $top ?? '' }}
|
||||
@@ -134,13 +134,13 @@
|
||||
|
||||
@if ($barItems->isNotEmpty())
|
||||
<div data-app-shell-bar class="fixed inset-x-0 bottom-0 z-30 sm:hidden">
|
||||
<x-navigation-bar :label="$label">
|
||||
<x-livewire-material::navigation-bar :label="$label">
|
||||
@foreach ($barItems as $item)
|
||||
<x-navigation-bar-item :label="$item['title']" :icon="$item['icon']" :link="$item['url']" :active="$item['active']" :badge="$item['badge']" :no-wire-navigate="! $item['navigate']" />
|
||||
<x-livewire-material::navigation-bar-item :label="$item['title']" :icon="$item['icon']" :link="$item['url']" :active="$item['active']" :badge="$item['badge']" :no-wire-navigate="! $item['navigate']" />
|
||||
@endforeach
|
||||
</x-navigation-bar>
|
||||
</x-livewire-material::navigation-bar>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<x-toast />
|
||||
<x-livewire-material::toast />
|
||||
</div>
|
||||
|
||||
@@ -202,13 +202,13 @@
|
||||
<{{ $tag }} {{ $attributes }}>
|
||||
@if ($spinnerTarget)
|
||||
<span wire:loading.flex wire:target="{{ $spinnerTarget }}" class="items-center justify-center">
|
||||
<x-loading :class="$iconSize" :label="false" />
|
||||
<x-livewire-material::loading :class="$iconSize" :label="false" />
|
||||
</span>
|
||||
@endif
|
||||
|
||||
@if ($icon)
|
||||
<span class="contents" @if ($spinnerTarget) wire:loading.remove wire:target="{{ $spinnerTarget }}" @endif>
|
||||
<x-icon :name="$icon" :filled="$selected === true" :class="\Illuminate\Support\Arr::toCssClasses([$iconSize, 'max-sm:size-6' => $fab])" />
|
||||
<x-livewire-material::icon :name="$icon" :filled="$selected === true" :class="\Illuminate\Support\Arr::toCssClasses([$iconSize, 'max-sm:size-6' => $fab])" />
|
||||
</span>
|
||||
@endif
|
||||
|
||||
@@ -217,10 +217,10 @@
|
||||
@endunless
|
||||
|
||||
@if ($iconRight)
|
||||
<x-icon :name="$iconRight" :class="$iconSize" />
|
||||
<x-livewire-material::icon :name="$iconRight" :class="$iconSize" />
|
||||
@endif
|
||||
|
||||
@if ($tip !== null)
|
||||
<x-tooltip :text="$tip" :side="$tipSide" :anchor="$anchor" />
|
||||
<x-livewire-material::tooltip :text="$tip" :side="$tipSide" :anchor="$anchor" />
|
||||
@endif
|
||||
</{{ $tag }}>
|
||||
|
||||
@@ -60,7 +60,7 @@
|
||||
</div>
|
||||
|
||||
@if ($separator)
|
||||
<x-divider class="mt-4" />
|
||||
<x-livewire-material::divider class="mt-4" />
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@@ -136,7 +136,7 @@
|
||||
'hidden pointer-fine:flex' => $controls === null,
|
||||
'flex' => $controls === true,
|
||||
])>
|
||||
<x-button
|
||||
<x-livewire-material::button
|
||||
icon="chevron_left"
|
||||
variant="tonal"
|
||||
:tooltip="__('Previous')"
|
||||
@@ -145,7 +145,7 @@
|
||||
x-on:click="previous()"
|
||||
class="rtl:-scale-x-100"
|
||||
/>
|
||||
<x-button
|
||||
<x-livewire-material::button
|
||||
icon="chevron_right"
|
||||
variant="tonal"
|
||||
:tooltip="__('Next')"
|
||||
|
||||
@@ -36,8 +36,8 @@
|
||||
@if ($indeterminate) data-indeterminate @endif
|
||||
@if ($messages !== []) aria-invalid="true" aria-describedby="{{ $id }}-support" @endif
|
||||
/>
|
||||
<x-icon name="check" class="size-4" data-check />
|
||||
<x-icon name="remove" class="size-4" data-mixed />
|
||||
<x-livewire-material::icon name="check" class="size-4" data-check />
|
||||
<x-livewire-material::icon name="remove" class="size-4" data-mixed />
|
||||
</span>
|
||||
|
||||
@if (filled($label) || filled($hint))
|
||||
|
||||
@@ -230,7 +230,7 @@
|
||||
<img src="{{ $avatar }}" alt="" @class(['me-2 size-6 shrink-0 rounded-corner-full object-cover', 'opacity-38' => $disabled]) />
|
||||
@endif
|
||||
@elseif ($icon)
|
||||
<x-icon :name="$icon" :class="\Illuminate\Support\Arr::toCssClasses(['me-2 size-4.5', $leadingInk])" />
|
||||
<x-livewire-material::icon :name="$icon" :class="\Illuminate\Support\Arr::toCssClasses(['me-2 size-4.5', $leadingInk])" />
|
||||
@endif
|
||||
|
||||
<span data-chip-label class="truncate">{{ $label ?? $slot }}</span>
|
||||
@@ -251,7 +251,7 @@
|
||||
'cursor-not-allowed' => $disabled,
|
||||
])
|
||||
>
|
||||
<x-icon name="close" class="size-4.5" />
|
||||
<x-livewire-material::icon name="close" class="size-4.5" />
|
||||
</button>
|
||||
@endif
|
||||
|
||||
@@ -260,7 +260,7 @@
|
||||
@endif
|
||||
|
||||
@if ($tooltip !== null)
|
||||
<x-tooltip :text="$tooltip" :anchor="$anchor" />
|
||||
<x-livewire-material::tooltip :text="$tooltip" :anchor="$anchor" />
|
||||
@endif
|
||||
</span>
|
||||
@else
|
||||
@@ -277,22 +277,22 @@
|
||||
'h-4.5 w-0 transition-[width] duration-(--md-sys-motion-effects-default-duration) ease-effects-default group-has-checked/chip:w-4.5 group-has-checked/chip:duration-(--md-sys-motion-spatial-fast-duration) group-has-checked/chip:ease-spatial-fast group-aria-pressed/chip:w-4.5 group-aria-pressed/chip:duration-(--md-sys-motion-spatial-fast-duration) group-aria-pressed/chip:ease-spatial-fast' => blank($icon),
|
||||
])>
|
||||
@if ($icon)
|
||||
<x-icon :name="$icon" :class="\Illuminate\Support\Arr::toCssClasses(['size-4.5 transition-opacity duration-(--md-sys-motion-effects-fast-duration) ease-effects-fast group-has-checked/chip:opacity-0 group-aria-pressed/chip:opacity-0', $leadingInk])" />
|
||||
<x-livewire-material::icon :name="$icon" :class="\Illuminate\Support\Arr::toCssClasses(['size-4.5 transition-opacity duration-(--md-sys-motion-effects-fast-duration) ease-effects-fast group-has-checked/chip:opacity-0 group-aria-pressed/chip:opacity-0', $leadingInk])" />
|
||||
@endif
|
||||
<x-icon name="check" data-chip-check :class="$check" />
|
||||
<x-livewire-material::icon name="check" data-chip-check :class="$check" />
|
||||
</span>
|
||||
@elseif ($icon)
|
||||
<x-icon :name="$icon" :class="\Illuminate\Support\Arr::toCssClasses(['me-2 size-4.5', $leadingInk])" />
|
||||
<x-livewire-material::icon :name="$icon" :class="\Illuminate\Support\Arr::toCssClasses(['me-2 size-4.5', $leadingInk])" />
|
||||
@endif
|
||||
|
||||
<span class="truncate">{{ $label ?? $slot }}</span>
|
||||
|
||||
@if ($iconRight)
|
||||
<x-icon :name="$iconRight" :class="\Illuminate\Support\Arr::toCssClasses(['ms-2 size-4.5', $trailingInk])" />
|
||||
<x-livewire-material::icon :name="$iconRight" :class="\Illuminate\Support\Arr::toCssClasses(['ms-2 size-4.5', $trailingInk])" />
|
||||
@endif
|
||||
|
||||
@if ($tooltip !== null)
|
||||
<x-tooltip :text="$tooltip" :anchor="$anchor" />
|
||||
<x-livewire-material::tooltip :text="$tooltip" :anchor="$anchor" />
|
||||
@endif
|
||||
</{{ $tag }}>
|
||||
@endif
|
||||
|
||||
@@ -115,7 +115,7 @@
|
||||
@if ($model === null) x-modelable="value" @endif
|
||||
>
|
||||
<div style="anchor-name: {{ $anchor }}">
|
||||
<x-field :$id :$label :$hint :$messages :$icon :$variant>
|
||||
<x-livewire-material::field :$id :$label :$hint :$messages :$icon :$variant>
|
||||
<input
|
||||
id="{{ $id }}"
|
||||
type="text"
|
||||
@@ -144,9 +144,9 @@
|
||||
/>
|
||||
|
||||
<x-slot:trailing>
|
||||
<x-icon name="arrow_drop_down" class="field-trailing field-arrow size-(--field-icon)" />
|
||||
<x-livewire-material::icon name="arrow_drop_down" class="field-trailing field-arrow size-(--field-icon)" />
|
||||
</x-slot:trailing>
|
||||
</x-field>
|
||||
</x-livewire-material::field>
|
||||
</div>
|
||||
|
||||
<ul
|
||||
@@ -170,7 +170,7 @@
|
||||
class="field-option"
|
||||
>
|
||||
<span x-text="option.label"></span>
|
||||
<x-icon name="check" class="field-check size-6" />
|
||||
<x-livewire-material::icon name="check" class="field-check size-6" />
|
||||
</li>
|
||||
</template>
|
||||
<li x-show="filtered.length === 0" class="px-4 py-3 type-body-md text-on-surface-variant">{{ __('Nothing matches') }}</li>
|
||||
@@ -202,9 +202,9 @@
|
||||
}"
|
||||
@if ($model === null) x-modelable="selection" @endif
|
||||
>
|
||||
<x-chip-set :$label :$hint :error-field="$model">
|
||||
<x-livewire-material::chip-set :$label :$hint :error-field="$model">
|
||||
@foreach ($choices as $index => $choice)
|
||||
<x-chip
|
||||
<x-livewire-material::chip
|
||||
type="filter"
|
||||
:label="$choice['label']"
|
||||
:selected="$isSelected($choice['value'])"
|
||||
@@ -213,6 +213,6 @@
|
||||
x-on:click="toggle({{ $index }})"
|
||||
/>
|
||||
@endforeach
|
||||
</x-chip-set>
|
||||
</x-livewire-material::chip-set>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@@ -28,12 +28,12 @@
|
||||
'px-4' => $variant === 'filled',
|
||||
])>
|
||||
@if ($icon)
|
||||
<x-icon :name="$icon" class="size-6 text-on-surface-variant" />
|
||||
<x-livewire-material::icon :name="$icon" class="size-6 text-on-surface-variant" />
|
||||
@endif
|
||||
|
||||
<span class="min-w-0 flex-1">{{ $heading ?? $title }}</span>
|
||||
|
||||
<x-icon name="expand_more" class="size-6 text-on-surface-variant transition-[rotate] duration-(--md-sys-motion-spatial-fast-duration) ease-spatial-fast group-open/collapse:rotate-180" />
|
||||
<x-livewire-material::icon name="expand_more" class="size-6 text-on-surface-variant transition-[rotate] duration-(--md-sys-motion-spatial-fast-duration) ease-spatial-fast group-open/collapse:rotate-180" />
|
||||
</summary>
|
||||
|
||||
<div @class(['pb-4 type-body-md text-on-surface-variant', 'px-4' => $variant === 'filled', 'pt-1'])>
|
||||
|
||||
@@ -21,7 +21,8 @@
|
||||
with, and the errors for `period`, `period.start` and `period.end` all belong to this field.
|
||||
`min` and `max` (`Y-m-d` or a date object) disable the days outside them and keep the
|
||||
keyboard inside them. `label`, `hint`, `icon`, `variant` (`outlined`, `filled`) and `size`
|
||||
are the field's; `name` adds hidden inputs carrying `Y-m-d` for a plain form post, and every
|
||||
are the field's; `clearable` adds a button that empties it (a date, or both ends of a range)
|
||||
once it holds one; `name` adds hidden inputs carrying `Y-m-d` for a plain form post, and every
|
||||
other attribute (`required`, `disabled`, `readonly`) reaches the text field. Errors under
|
||||
the `wire:model` name replace the hint, and so does a typed date that cannot be read.
|
||||
|
||||
@@ -48,6 +49,7 @@
|
||||
'min' => null,
|
||||
'max' => null,
|
||||
'value' => null,
|
||||
'clearable' => false,
|
||||
])
|
||||
|
||||
@php
|
||||
@@ -133,7 +135,7 @@
|
||||
data-datepicker
|
||||
>
|
||||
<div style="anchor-name: {{ $anchor }}">
|
||||
<x-field
|
||||
<x-livewire-material::field
|
||||
:$id
|
||||
:$label
|
||||
:$icon
|
||||
@@ -179,6 +181,19 @@
|
||||
/>
|
||||
|
||||
<x-slot:trailing>
|
||||
@if ($clearable)
|
||||
<button
|
||||
type="button"
|
||||
class="field-trailing field-clear field-button"
|
||||
aria-label="{{ __('Clear') }}"
|
||||
x-on:click="clear()"
|
||||
@disabled($attributes->get('disabled') || $attributes->get('readonly'))
|
||||
data-field-clear
|
||||
>
|
||||
<x-livewire-material::icon name="close" class="size-(--field-icon)" />
|
||||
</button>
|
||||
@endif
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="field-trailing field-button"
|
||||
@@ -191,10 +206,10 @@
|
||||
@disabled($attributes->get('disabled') || $attributes->get('readonly'))
|
||||
data-datepicker-toggle
|
||||
>
|
||||
<x-icon name="calendar_today" class="size-(--field-icon)" />
|
||||
<x-livewire-material::icon name="calendar_today" class="size-(--field-icon)" />
|
||||
</button>
|
||||
</x-slot:trailing>
|
||||
</x-field>
|
||||
</x-livewire-material::field>
|
||||
</div>
|
||||
|
||||
<div id="{{ $id }}-support" data-datepicker-support data-size="{{ in_array($size, ['sm', 'xs'], true) ? $size : 'md' }}">
|
||||
@@ -241,10 +256,10 @@
|
||||
<p data-datepicker-headline aria-live="polite" x-text="headline"></p>
|
||||
|
||||
<span x-show="! typing">
|
||||
<x-button icon="edit" :tooltip="__('Switch to text input mode')" x-on:click="toggleTyping()" data-datepicker-switch />
|
||||
<x-livewire-material::button icon="edit" :tooltip="__('Switch to text input mode')" x-on:click="toggleTyping()" data-datepicker-switch />
|
||||
</span>
|
||||
<span x-show="typing">
|
||||
<x-button icon="date_range" :tooltip="__('Switch to calendar input mode')" x-on:click="toggleTyping()" data-datepicker-switch />
|
||||
<x-livewire-material::button icon="date_range" :tooltip="__('Switch to calendar input mode')" x-on:click="toggleTyping()" data-datepicker-switch />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -261,19 +276,19 @@
|
||||
x-bind:aria-label="monthYear + ', ' + @js(__('Switch to selecting a year'))"
|
||||
>
|
||||
<span x-text="monthYear"></span>
|
||||
<x-icon name="arrow_drop_down" class="size-4.5" data-datepicker-menu-arrow />
|
||||
<x-livewire-material::icon name="arrow_drop_down" class="size-4.5" data-datepicker-menu-arrow />
|
||||
</button>
|
||||
|
||||
<span data-datepicker-arrows x-bind:data-concealed="view !== 'days' ? '' : null">
|
||||
<x-button icon="chevron_left" :tooltip="__('Previous month')" x-on:click="step(-1)" x-bind:disabled="view !== 'days' || ! canStep(-1)" data-datepicker-previous />
|
||||
<x-button icon="chevron_right" :tooltip="__('Next month')" x-on:click="step(1)" x-bind:disabled="view !== 'days' || ! canStep(1)" data-datepicker-next />
|
||||
<x-livewire-material::button icon="chevron_left" :tooltip="__('Previous month')" x-on:click="step(-1)" x-bind:disabled="view !== 'days' || ! canStep(-1)" data-datepicker-previous />
|
||||
<x-livewire-material::button icon="chevron_right" :tooltip="__('Next month')" x-on:click="step(1)" x-bind:disabled="view !== 'days' || ! canStep(1)" data-datepicker-next />
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div data-datepicker-nav data-docked x-show="presentation === 'docked'">
|
||||
<span data-datepicker-stepper>
|
||||
<span data-datepicker-arrows x-bind:data-concealed="view !== 'days' ? '' : null">
|
||||
<x-button icon="chevron_left" :tooltip="__('Previous month')" x-on:click="step(-1)" x-bind:disabled="view !== 'days' || ! canStep(-1)" />
|
||||
<x-livewire-material::button icon="chevron_left" :tooltip="__('Previous month')" x-on:click="step(-1)" x-bind:disabled="view !== 'days' || ! canStep(-1)" />
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
@@ -283,16 +298,16 @@
|
||||
x-bind:aria-label="monthLabel + ', ' + @js(__('Switch to selecting a month'))"
|
||||
>
|
||||
<span x-text="monthLabel"></span>
|
||||
<x-icon name="arrow_drop_down" class="size-4.5" data-datepicker-menu-arrow />
|
||||
<x-livewire-material::icon name="arrow_drop_down" class="size-4.5" data-datepicker-menu-arrow />
|
||||
</button>
|
||||
<span data-datepicker-arrows x-bind:data-concealed="view !== 'days' ? '' : null">
|
||||
<x-button icon="chevron_right" :tooltip="__('Next month')" x-on:click="step(1)" x-bind:disabled="view !== 'days' || ! canStep(1)" />
|
||||
<x-livewire-material::button icon="chevron_right" :tooltip="__('Next month')" x-on:click="step(1)" x-bind:disabled="view !== 'days' || ! canStep(1)" />
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<span data-datepicker-stepper>
|
||||
<span data-datepicker-arrows x-bind:data-concealed="view !== 'days' ? '' : null">
|
||||
<x-button icon="chevron_left" :tooltip="__('Previous year')" x-on:click="step(-12)" x-bind:disabled="view !== 'days' || ! canStep(-12)" />
|
||||
<x-livewire-material::button icon="chevron_left" :tooltip="__('Previous year')" x-on:click="step(-12)" x-bind:disabled="view !== 'days' || ! canStep(-12)" />
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
@@ -302,10 +317,10 @@
|
||||
x-bind:aria-label="yearLabel + ', ' + @js(__('Switch to selecting a year'))"
|
||||
>
|
||||
<span x-text="yearLabel"></span>
|
||||
<x-icon name="arrow_drop_down" class="size-4.5" data-datepicker-menu-arrow />
|
||||
<x-livewire-material::icon name="arrow_drop_down" class="size-4.5" data-datepicker-menu-arrow />
|
||||
</button>
|
||||
<span data-datepicker-arrows x-bind:data-concealed="view !== 'days' ? '' : null">
|
||||
<x-button icon="chevron_right" :tooltip="__('Next year')" x-on:click="step(12)" x-bind:disabled="view !== 'days' || ! canStep(12)" />
|
||||
<x-livewire-material::button icon="chevron_right" :tooltip="__('Next year')" x-on:click="step(12)" x-bind:disabled="view !== 'days' || ! canStep(12)" />
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
@@ -372,7 +387,7 @@
|
||||
x-on:click="month.disabled || showMonthOf(+shown.slice(0, 4), month.value)"
|
||||
data-datepicker-option
|
||||
>
|
||||
<x-icon name="check" data-datepicker-check />
|
||||
<x-livewire-material::icon name="check" data-datepicker-check />
|
||||
<span x-text="month.label"></span>
|
||||
</button>
|
||||
</template>
|
||||
@@ -388,7 +403,7 @@
|
||||
x-on:click="showMonthOf(year.value, +shown.slice(5, 7))"
|
||||
data-datepicker-option
|
||||
>
|
||||
<x-icon name="check" data-datepicker-check />
|
||||
<x-livewire-material::icon name="check" data-datepicker-check />
|
||||
<span x-text="year.label"></span>
|
||||
</button>
|
||||
</template>
|
||||
@@ -397,7 +412,7 @@
|
||||
|
||||
<div x-show="typing" data-datepicker-entry>
|
||||
<div @if ($range) data-range @endif data-datepicker-entry-fields>
|
||||
<x-field id="{{ $id }}-entry" :label="$range ? __('Start date') : __('Date')" :$variant x-bind:data-invalid="entryError !== '' ? '' : null">
|
||||
<x-livewire-material::field id="{{ $id }}-entry" :label="$range ? __('Start date') : __('Date')" :$variant x-bind:data-invalid="entryError !== '' ? '' : null">
|
||||
<input
|
||||
id="{{ $id }}-entry"
|
||||
type="text"
|
||||
@@ -412,10 +427,10 @@
|
||||
x-on:keydown.enter.prevent="confirm()"
|
||||
class="field-control"
|
||||
/>
|
||||
</x-field>
|
||||
</x-livewire-material::field>
|
||||
|
||||
@if ($range)
|
||||
<x-field id="{{ $id }}-entry-end" :label="__('End date')" :$variant x-bind:data-invalid="entryError !== '' ? '' : null">
|
||||
<x-livewire-material::field id="{{ $id }}-entry-end" :label="__('End date')" :$variant x-bind:data-invalid="entryError !== '' ? '' : null">
|
||||
<input
|
||||
id="{{ $id }}-entry-end"
|
||||
type="text"
|
||||
@@ -429,7 +444,7 @@
|
||||
x-on:keydown.enter.prevent="confirm()"
|
||||
class="field-control"
|
||||
/>
|
||||
</x-field>
|
||||
</x-livewire-material::field>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@@ -439,8 +454,8 @@
|
||||
</div>
|
||||
|
||||
<div x-show="view === 'days' || typing || presentation === 'modal'" data-datepicker-actions>
|
||||
<x-button :label="__('Cancel')" x-on:click="cancel()" data-datepicker-cancel />
|
||||
<x-button :label="__('OK')" x-on:click="confirm()" data-datepicker-confirm />
|
||||
<x-livewire-material::button :label="__('Cancel')" x-on:click="cancel()" data-datepicker-cancel />
|
||||
<x-livewire-material::button :label="__('OK')" x-on:click="confirm()" data-datepicker-confirm />
|
||||
</div>
|
||||
</div>
|
||||
</dialog>
|
||||
|
||||
@@ -113,13 +113,13 @@
|
||||
|
||||
@if ($withCloseButton)
|
||||
<span class="-me-3 -mt-2 inline-flex shrink-0">
|
||||
<x-button icon="close" :tooltip-left="__('Close')" x-on:click="close()" />
|
||||
<x-livewire-material::button icon="close" :tooltip-left="__('Close')" x-on:click="close()" />
|
||||
</span>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@if ($separator)
|
||||
<x-divider class="mt-4" />
|
||||
<x-livewire-material::divider class="mt-4" />
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@@ -19,8 +19,8 @@
|
||||
|
||||
<div {{ $attributes->class('flex flex-col items-center gap-4 px-4 py-10 text-center') }}>
|
||||
<div class="relative grid size-28 place-items-center">
|
||||
<x-shape :name="$shape" class="absolute inset-0 size-full text-secondary-container" />
|
||||
<x-icon :name="$icon" class="relative size-12 text-on-secondary-container" />
|
||||
<x-livewire-material::shape :name="$shape" class="absolute inset-0 size-full text-secondary-container" />
|
||||
<x-livewire-material::icon :name="$icon" class="relative size-12 text-on-secondary-container" />
|
||||
</div>
|
||||
|
||||
@if ($title)
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
|
||||
<{{ $tag }} {{ $attributes }}>
|
||||
@if ($icon)
|
||||
<x-icon :name="$icon" class="size-6" />
|
||||
<x-livewire-material::icon :name="$icon" class="size-6" />
|
||||
@endif
|
||||
|
||||
<span>{{ $label ?? $slot }}</span>
|
||||
|
||||
@@ -49,8 +49,8 @@
|
||||
$colours,
|
||||
])
|
||||
>
|
||||
<x-icon :name="$icon" class="size-6 group-aria-expanded/fab:hidden" />
|
||||
<x-icon name="close" class="hidden size-5 group-aria-expanded/fab:block" />
|
||||
<x-livewire-material::icon :name="$icon" class="size-6 group-aria-expanded/fab:hidden" />
|
||||
<x-livewire-material::icon name="close" class="hidden size-5 group-aria-expanded/fab:block" />
|
||||
</button>
|
||||
</span>
|
||||
|
||||
|
||||
@@ -65,7 +65,7 @@
|
||||
|
||||
<{{ $tag }} {{ $attributes }}>
|
||||
@if ($icon)
|
||||
<x-icon :name="$icon" :class="$iconSize" />
|
||||
<x-livewire-material::icon :name="$icon" :class="$iconSize" />
|
||||
@endif
|
||||
|
||||
@if ($extended)
|
||||
@@ -73,6 +73,6 @@
|
||||
@endif
|
||||
|
||||
@if ($tooltip !== null)
|
||||
<x-tooltip :text="$tooltip" :anchor="$anchor" />
|
||||
<x-livewire-material::tooltip :text="$tooltip" :anchor="$anchor" />
|
||||
@endif
|
||||
</{{ $tag }}>
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
>
|
||||
<div class="field-box">
|
||||
@if ($icon)
|
||||
<x-icon :name="$icon" class="field-icon size-(--field-icon)" />
|
||||
<x-livewire-material::icon :name="$icon" class="field-icon size-(--field-icon)" />
|
||||
@endif
|
||||
|
||||
@if (filled($prefix))
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
: [];
|
||||
@endphp
|
||||
|
||||
<x-field :$id :$label :$hint :$messages :$variant floated :class="$attributes->get('class')">
|
||||
<x-livewire-material::field :$id :$label :$hint :$messages :$variant floated :class="$attributes->get('class')">
|
||||
<input
|
||||
{{ $attributes->except(['class', 'id', 'type']) }}
|
||||
type="file"
|
||||
@@ -35,4 +35,4 @@
|
||||
@if ($messages !== [] || filled($hint)) aria-describedby="{{ $id }}-support" @endif
|
||||
class="field-control"
|
||||
/>
|
||||
</x-field>
|
||||
</x-livewire-material::field>
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
@isset($actions)
|
||||
@if ($separator)
|
||||
<x-divider />
|
||||
<x-livewire-material::divider />
|
||||
@endif
|
||||
|
||||
<div {{ $actions->attributes->class(['flex flex-wrap items-center justify-end gap-2']) }}>
|
||||
|
||||
@@ -79,7 +79,7 @@
|
||||
/>
|
||||
|
||||
@if (filled(data_get($option, $optionIcon)))
|
||||
<x-icon :name="data_get($option, $optionIcon)" :class="$iconSize" />
|
||||
<x-livewire-material::icon :name="data_get($option, $optionIcon)" :class="$iconSize" />
|
||||
@endif
|
||||
|
||||
<span class="truncate">{{ data_get($option, $optionLabel) }}</span>
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
$messages = $model !== null && isset($errors) ? \Illuminate\Support\Arr::flatten($errors->get($model)) : [];
|
||||
@endphp
|
||||
|
||||
<x-field :$id :$label :$hint :hint-class="$hintClass" :$messages :$icon :$prefix :$suffix :$size :$variant :$mono :class="$attributes->get('class')" :data-readonly="$attributes->get('readonly') ? '' : null">
|
||||
<x-livewire-material::field :$id :$label :$hint :hint-class="$hintClass" :$messages :$icon :$prefix :$suffix :$size :$variant :$mono :class="$attributes->get('class')" :data-readonly="$attributes->get('readonly') ? '' : null">
|
||||
<input
|
||||
{{ $attributes->except(['class', 'id', 'placeholder'])->merge(['type' => 'text']) }}
|
||||
id="{{ $id }}"
|
||||
@@ -54,7 +54,7 @@
|
||||
aria-label="{{ __('Clear') }}"
|
||||
data-field-clear
|
||||
>
|
||||
<x-icon name="close" class="size-(--field-icon)" />
|
||||
<x-livewire-material::icon name="close" class="size-(--field-icon)" />
|
||||
</button>
|
||||
@endif
|
||||
|
||||
@@ -67,13 +67,13 @@
|
||||
aria-label="{{ __('Copy') }}"
|
||||
data-field-copy
|
||||
>
|
||||
<x-icon name="content_copy" class="size-(--field-icon)" />
|
||||
<x-livewire-material::icon name="content_copy" class="size-(--field-icon)" />
|
||||
</button>
|
||||
@endif
|
||||
|
||||
@if ($iconRight)
|
||||
<x-icon :name="$iconRight" class="field-trailing size-(--field-icon)" />
|
||||
<x-livewire-material::icon :name="$iconRight" class="field-trailing size-(--field-icon)" />
|
||||
@endif
|
||||
</x-slot:trailing>
|
||||
@endif
|
||||
</x-field>
|
||||
</x-livewire-material::field>
|
||||
|
||||
@@ -61,7 +61,7 @@
|
||||
@elseif ($image)
|
||||
<img src="{{ $image }}" alt="" class="size-14 shrink-0 rounded-corner-sm object-cover" />
|
||||
@elseif ($icon)
|
||||
<x-icon :name="$icon" :class="\Illuminate\Support\Arr::toCssClasses(['size-6', 'text-on-surface-variant' => ! $selected && ! $disabled])" />
|
||||
<x-livewire-material::icon :name="$icon" :class="\Illuminate\Support\Arr::toCssClasses(['size-6', 'text-on-surface-variant' => ! $selected && ! $disabled])" />
|
||||
@endif
|
||||
|
||||
<div class="min-w-0 flex-1">
|
||||
@@ -94,6 +94,6 @@
|
||||
@endif
|
||||
|
||||
@if ($iconRight)
|
||||
<x-icon :name="$iconRight" :class="\Illuminate\Support\Arr::toCssClasses(['size-6', 'text-on-surface-variant' => ! $selected && ! $disabled])" />
|
||||
<x-livewire-material::icon :name="$iconRight" :class="\Illuminate\Support\Arr::toCssClasses(['size-6', 'text-on-surface-variant' => ! $selected && ! $disabled])" />
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@@ -60,7 +60,7 @@
|
||||
|
||||
<{{ $tag }} {{ $attributes }}>
|
||||
@if ($icon)
|
||||
<x-icon :name="$icon" :filled="$selected === true" :class="'size-5 '.$iconInk" />
|
||||
<x-livewire-material::icon :name="$icon" :filled="$selected === true" :class="'size-5 '.$iconInk" />
|
||||
@endif
|
||||
|
||||
<span class="min-w-0 flex-1">
|
||||
@@ -76,6 +76,6 @@
|
||||
@endif
|
||||
|
||||
@if ($iconRight)
|
||||
<x-icon :name="$iconRight" :class="'size-5 '.$iconInk" />
|
||||
<x-livewire-material::icon :name="$iconRight" :class="'size-5 '.$iconInk" />
|
||||
@endif
|
||||
</{{ $tag }}>
|
||||
|
||||
@@ -69,7 +69,7 @@
|
||||
])>
|
||||
@if ($fullscreen)
|
||||
<div class="flex h-16 shrink-0 items-center gap-1 px-1 sm:hidden">
|
||||
<x-button icon="close" :tooltip="__('Close')" x-on:click="close()" />
|
||||
<x-livewire-material::button icon="close" :tooltip="__('Close')" x-on:click="close()" />
|
||||
|
||||
@if (filled($title))
|
||||
<span class="truncate type-title-lg">{{ $title }}</span>
|
||||
@@ -81,7 +81,7 @@
|
||||
@if (filled($title) || $icon)
|
||||
<div @class(['mb-4', 'max-sm:hidden' => $fullscreen && ! $icon, 'text-center' => $icon])>
|
||||
@if ($icon)
|
||||
<x-icon :name="$icon" class="mx-auto mb-4 size-6 text-secondary" />
|
||||
<x-livewire-material::icon :name="$icon" class="mx-auto mb-4 size-6 text-secondary" />
|
||||
@endif
|
||||
|
||||
@if (filled($title))
|
||||
@@ -93,7 +93,7 @@
|
||||
@endif
|
||||
|
||||
@if ($separator)
|
||||
<x-divider class="mt-4" />
|
||||
<x-livewire-material::divider class="mt-4" />
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@@ -49,13 +49,13 @@
|
||||
<span data-navigation-indicator>
|
||||
<span class="relative inline-flex">
|
||||
@if ($icon)
|
||||
<x-icon :name="$icon" :filled="$active" class="size-6" />
|
||||
<x-livewire-material::icon :name="$icon" :filled="$active" class="size-6" />
|
||||
@endif
|
||||
|
||||
@if ($dot)
|
||||
<x-badge floating />
|
||||
<x-livewire-material::badge floating />
|
||||
@elseif ($count)
|
||||
<x-badge :value="$badge" max="999" floating />
|
||||
<x-livewire-material::badge :value="$badge" max="999" floating />
|
||||
@endif
|
||||
</span>
|
||||
</span>
|
||||
|
||||
@@ -51,13 +51,13 @@
|
||||
<span data-navigation-indicator>
|
||||
<span class="relative inline-flex">
|
||||
@if ($icon)
|
||||
<x-icon :name="$icon" :filled="$active" class="size-6" />
|
||||
<x-livewire-material::icon :name="$icon" :filled="$active" class="size-6" />
|
||||
@endif
|
||||
|
||||
@if ($dot)
|
||||
<x-badge floating />
|
||||
<x-livewire-material::badge floating />
|
||||
@elseif ($count)
|
||||
<span class="hidden rail-collapsed:contents"><x-badge :value="$badge" max="999" floating /></span>
|
||||
<span class="hidden rail-collapsed:contents"><x-livewire-material::badge :value="$badge" max="999" floating /></span>
|
||||
@endif
|
||||
</span>
|
||||
</span>
|
||||
@@ -65,7 +65,7 @@
|
||||
<span data-navigation-label>{{ $label ?? $slot }}</span>
|
||||
|
||||
@if ($count)
|
||||
<span class="flex shrink-0 rail-collapsed:hidden"><x-badge :value="$badge" max="999" /></span>
|
||||
<span class="flex shrink-0 rail-collapsed:hidden"><x-livewire-material::badge :value="$badge" max="999" /></span>
|
||||
@endif
|
||||
|
||||
@if ($spoken !== null)
|
||||
|
||||
@@ -107,8 +107,8 @@
|
||||
x-bind:aria-expanded="expanded.toString()"
|
||||
class="state-layer focus-ring inline-flex size-10 shrink-0 cursor-pointer items-center justify-center rounded-corner-full text-on-surface-variant after:absolute after:top-1/2 after:left-1/2 after:size-12 after:-translate-x-1/2 after:-translate-y-1/2"
|
||||
>
|
||||
<span class="contents rail-collapsed:hidden"><x-icon name="menu_open" class="size-6" /></span>
|
||||
<span class="hidden rail-collapsed:contents"><x-icon name="menu" class="size-6" /></span>
|
||||
<span class="contents rail-collapsed:hidden"><x-livewire-material::icon name="menu_open" class="size-6" /></span>
|
||||
<span class="hidden rail-collapsed:contents"><x-livewire-material::icon name="menu" class="size-6" /></span>
|
||||
</button>
|
||||
@endif
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
$messages = $model !== null && isset($errors) ? \Illuminate\Support\Arr::flatten($errors->get($model)) : [];
|
||||
@endphp
|
||||
|
||||
<x-field :$id :$label :$hint :hint-class="$hintClass" :$messages :$icon :$size :$variant :class="$attributes->get('class')" x-data="{ shown: false }">
|
||||
<x-livewire-material::field :$id :$label :$hint :hint-class="$hintClass" :$messages :$icon :$size :$variant :class="$attributes->get('class')" x-data="{ shown: false }">
|
||||
<input
|
||||
{{ $attributes->except(['class', 'id', 'type', 'placeholder']) }}
|
||||
id="{{ $id }}"
|
||||
@@ -43,8 +43,8 @@
|
||||
class="field-trailing field-button"
|
||||
data-field-reveal
|
||||
>
|
||||
<x-icon name="visibility" class="size-(--field-icon)" x-show="! shown" />
|
||||
<x-icon name="visibility_off" class="size-(--field-icon)" x-show="shown" x-cloak />
|
||||
<x-livewire-material::icon name="visibility" class="size-(--field-icon)" x-show="! shown" />
|
||||
<x-livewire-material::icon name="visibility_off" class="size-(--field-icon)" x-show="shown" x-cloak />
|
||||
</button>
|
||||
</x-slot:trailing>
|
||||
</x-field>
|
||||
</x-livewire-material::field>
|
||||
|
||||
@@ -37,11 +37,11 @@
|
||||
>
|
||||
<div data-search-bar role="search" x-on:click="if ($event.target === $el) $refs.input.focus()">
|
||||
<span data-search-leading x-show="! fullScreen">
|
||||
<x-icon :name="$icon" />
|
||||
<x-livewire-material::icon :name="$icon" />
|
||||
</span>
|
||||
|
||||
<button type="button" data-search-leading data-search-back x-show="fullScreen" x-cloak x-on:click="close()" aria-label="{{ __('Back') }}">
|
||||
<x-icon name="arrow_back" />
|
||||
<x-livewire-material::icon name="arrow_back" />
|
||||
</button>
|
||||
|
||||
<input
|
||||
@@ -64,7 +64,7 @@
|
||||
/>
|
||||
|
||||
<button type="button" data-search-clear x-on:click="clear()" aria-label="{{ __('Clear') }}">
|
||||
<x-icon name="close" />
|
||||
<x-livewire-material::icon name="close" />
|
||||
</button>
|
||||
|
||||
@isset($trailing)
|
||||
|
||||
@@ -33,21 +33,21 @@
|
||||
<div {{ $attributes->class(['min-w-0']) }} data-section-nav>
|
||||
@if ($current)
|
||||
<div class="sm:hidden" data-section-picker>
|
||||
<x-menu :$label position="bottom-start">
|
||||
<x-livewire-material::menu :$label position="bottom-start">
|
||||
<x-slot:trigger>
|
||||
<button type="button" class="focus-ring flex h-12 w-[min(20rem,calc(100vw-2rem))] cursor-pointer items-center gap-3 rounded-corner-xs border border-outline px-4 text-start type-body-lg text-on-surface">
|
||||
@isset($current['icon'])
|
||||
<x-icon :name="$current['icon']" class="size-5 text-on-surface-variant" />
|
||||
<x-livewire-material::icon :name="$current['icon']" class="size-5 text-on-surface-variant" />
|
||||
@endisset
|
||||
<span class="min-w-0 flex-1 truncate">{{ $current['title'] }}</span>
|
||||
<x-icon name="arrow_drop_down" class="size-6 text-on-surface-variant" />
|
||||
<x-livewire-material::icon name="arrow_drop_down" class="size-6 text-on-surface-variant" />
|
||||
</button>
|
||||
</x-slot:trigger>
|
||||
|
||||
@foreach ($items as $item)
|
||||
<x-menu-item :label="$item['title']" :icon="$item['icon'] ?? null" :link="$item['url']" :selected="$isCurrent($item)" :no-wire-navigate="$noWireNavigate" />
|
||||
<x-livewire-material::menu-item :label="$item['title']" :icon="$item['icon'] ?? null" :link="$item['url']" :selected="$isCurrent($item)" :no-wire-navigate="$noWireNavigate" />
|
||||
@endforeach
|
||||
</x-menu>
|
||||
</x-livewire-material::menu>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@@ -65,11 +65,11 @@
|
||||
>
|
||||
<span data-tab-content>
|
||||
@isset($item['icon'])
|
||||
<x-icon :name="$item['icon']" :filled="$on" class="size-5" />
|
||||
<x-livewire-material::icon :name="$item['icon']" :filled="$on" class="size-5" />
|
||||
@endisset
|
||||
<span class="truncate">{{ $item['title'] }}</span>
|
||||
@if (filled($item['badge'] ?? null))
|
||||
<x-badge :value="$item['badge']" />
|
||||
<x-livewire-material::badge :value="$item['badge']" />
|
||||
@endif
|
||||
<span data-tab-indicator aria-hidden="true"></span>
|
||||
</span>
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
$messages = $model !== null && isset($errors) ? \Illuminate\Support\Arr::flatten($errors->get($model)) : [];
|
||||
@endphp
|
||||
|
||||
<x-field :$id :$label :$hint :hint-class="$hintClass" :$messages :$icon :$size :$variant floated :class="$attributes->get('class')">
|
||||
<x-livewire-material::field :$id :$label :$hint :hint-class="$hintClass" :$messages :$icon :$size :$variant floated :class="$attributes->get('class')">
|
||||
<select
|
||||
{{ $attributes->except(['class', 'id']) }}
|
||||
id="{{ $id }}"
|
||||
@@ -52,6 +52,6 @@
|
||||
</select>
|
||||
|
||||
<x-slot:trailing>
|
||||
<x-icon name="arrow_drop_down" class="field-trailing field-arrow size-(--field-icon)" />
|
||||
<x-livewire-material::icon name="arrow_drop_down" class="field-trailing field-arrow size-(--field-icon)" />
|
||||
</x-slot:trailing>
|
||||
</x-field>
|
||||
</x-livewire-material::field>
|
||||
|
||||
@@ -321,7 +321,7 @@
|
||||
@class(['absolute top-1/2 flex -translate-y-1/2 rtl:-scale-x-100', $iconInk[$color], 'group-has-disabled/slider:text-on-surface/38'])
|
||||
style="left: {{ $calc($iconAt) }}"
|
||||
>
|
||||
<x-icon :name="$icon" :class="$iconSize === 32 ? 'size-8' : 'size-6'" />
|
||||
<x-livewire-material::icon :name="$icon" :class="$iconSize === 32 ? 'size-8' : 'size-6'" />
|
||||
</span>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
])
|
||||
>
|
||||
{{ $slot }}
|
||||
<x-icon :name="$direction === 'desc' ? 'arrow_downward' : 'arrow_upward'" :class="\Illuminate\Support\Arr::toCssClasses([
|
||||
<x-livewire-material::icon :name="$direction === 'desc' ? 'arrow_downward' : 'arrow_upward'" :class="\Illuminate\Support\Arr::toCssClasses([
|
||||
'size-4 transition-opacity duration-(--md-sys-motion-effects-fast-duration)',
|
||||
'opacity-0 group-hover/sort:opacity-60 group-focus-visible/sort:opacity-60' => ! $active,
|
||||
])" />
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
@endphp
|
||||
|
||||
<div data-button-group="split" data-size="{{ $size }}" {{ $attributes->only('class')->class('inline-flex items-center gap-0.5') }}>
|
||||
<x-button
|
||||
<x-livewire-material::button
|
||||
{{ $attributes->except('class') }}
|
||||
:label="$label"
|
||||
:icon="$icon"
|
||||
@@ -51,9 +51,9 @@
|
||||
:class="$leadingPadding"
|
||||
/>
|
||||
|
||||
<x-menu :position="$position" :label="$menuLabel">
|
||||
<x-livewire-material::menu :position="$position" :label="$menuLabel">
|
||||
<x-slot:trigger>
|
||||
<x-button
|
||||
<x-livewire-material::button
|
||||
icon="keyboard_arrow_down"
|
||||
:aria-label="$menuLabel"
|
||||
:variant="$variant"
|
||||
@@ -67,5 +67,5 @@
|
||||
</x-slot:trigger>
|
||||
|
||||
{{ $slot }}
|
||||
</x-menu>
|
||||
</x-livewire-material::menu>
|
||||
</div>
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<div {{ $attributes->class('flex flex-col gap-1 rounded-corner-lg bg-surface-container p-4') }}>
|
||||
<div class="flex items-center gap-2 type-label-lg text-on-surface-variant">
|
||||
@if ($icon)
|
||||
<x-icon :name="$icon" class="size-5" />
|
||||
<x-livewire-material::icon :name="$icon" class="size-5" />
|
||||
@endif
|
||||
<span>{{ $title }}</span>
|
||||
</div>
|
||||
|
||||
@@ -72,12 +72,12 @@
|
||||
>
|
||||
<span data-tab-content>
|
||||
@isset($tab['icon'])
|
||||
<x-icon :name="$tab['icon']" class="size-6" />
|
||||
<x-livewire-material::icon :name="$tab['icon']" class="size-6" />
|
||||
@endisset
|
||||
<span class="inline-flex items-center gap-1.5">
|
||||
{{ $tab['label'] }}
|
||||
@if (filled($tab['badge'] ?? null))
|
||||
<x-badge :value="$tab['badge']" />
|
||||
<x-livewire-material::badge :value="$tab['badge']" />
|
||||
@endif
|
||||
</span>
|
||||
<span data-tab-indicator aria-hidden="true"></span>
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
$messages = $model !== null && isset($errors) ? \Illuminate\Support\Arr::flatten($errors->get($model)) : [];
|
||||
@endphp
|
||||
|
||||
<x-field :$id :$label :$hint :hint-class="$hintClass" :$messages :$variant :class="$attributes->get('class')" :data-readonly="$attributes->get('readonly') ? '' : null">
|
||||
<x-livewire-material::field :$id :$label :$hint :hint-class="$hintClass" :$messages :$variant :class="$attributes->get('class')" :data-readonly="$attributes->get('readonly') ? '' : null">
|
||||
<textarea
|
||||
{{ $attributes->except(['class', 'id', 'placeholder']) }}
|
||||
id="{{ $id }}"
|
||||
@@ -36,4 +36,4 @@
|
||||
@if ($messages !== [] || filled($hint)) aria-describedby="{{ $id }}-support" @endif
|
||||
class="field-control"
|
||||
></textarea>
|
||||
</x-field>
|
||||
</x-livewire-material::field>
|
||||
|
||||
@@ -40,8 +40,8 @@
|
||||
data-theme-option="{{ $choice }}"
|
||||
class="state-layer focus-ring inline-flex min-w-0 flex-1 cursor-pointer items-center justify-center gap-2 border-outline px-3 type-label-lg text-on-surface not-first:border-s first:rounded-s-corner-full last:rounded-e-corner-full aria-checked:bg-secondary-container aria-checked:text-on-secondary-container"
|
||||
>
|
||||
<x-icon name="check" class="hidden size-4.5 in-aria-checked:block" />
|
||||
<x-icon :name="$icon" class="size-4.5 in-aria-checked:hidden" />
|
||||
<x-livewire-material::icon name="check" class="hidden size-4.5 in-aria-checked:block" />
|
||||
<x-livewire-material::icon :name="$icon" class="size-4.5 in-aria-checked:hidden" />
|
||||
{{ __($text) }}
|
||||
</button>
|
||||
@endforeach
|
||||
@@ -65,12 +65,12 @@
|
||||
{{ $attributes->class(['state-layer focus-ring inline-flex size-10 shrink-0 cursor-pointer items-center justify-center rounded-corner-full text-on-surface-variant transition-[border-radius] duration-(--md-sys-motion-spatial-fast-duration) ease-spatial-fast active:rounded-corner-sm']) }}
|
||||
>
|
||||
@if ($mode === 'cycle')
|
||||
<x-icon name="light_mode" x-cloak x-show="$store.theme.choice === 'light'" />
|
||||
<x-icon name="dark_mode" x-cloak x-show="$store.theme.choice === 'dark'" />
|
||||
<x-icon name="brightness_auto" x-cloak x-show="$store.theme.choice === 'system'" />
|
||||
<x-livewire-material::icon name="light_mode" x-cloak x-show="$store.theme.choice === 'light'" />
|
||||
<x-livewire-material::icon name="dark_mode" x-cloak x-show="$store.theme.choice === 'dark'" />
|
||||
<x-livewire-material::icon name="brightness_auto" x-cloak x-show="$store.theme.choice === 'system'" />
|
||||
@else
|
||||
<x-icon name="light_mode" x-cloak x-show="$store.theme.resolved === 'dark'" />
|
||||
<x-icon name="dark_mode" x-cloak x-show="$store.theme.resolved !== 'dark'" />
|
||||
<x-livewire-material::icon name="light_mode" x-cloak x-show="$store.theme.resolved === 'dark'" />
|
||||
<x-livewire-material::icon name="dark_mode" x-cloak x-show="$store.theme.resolved !== 'dark'" />
|
||||
@endif
|
||||
</button>
|
||||
@endif
|
||||
|
||||
@@ -138,7 +138,7 @@
|
||||
x-data="materialTimepicker(@if ($model !== null) @entangle($attributes->wire('model')) @else @js($current) @endif, @js($config))"
|
||||
@if ($model === null) x-modelable="value" @endif
|
||||
>
|
||||
<x-field :$id :$label :$hint :$messages :$icon :$size :$variant data-timepicker-field>
|
||||
<x-livewire-material::field :$id :$label :$hint :$messages :$icon :$size :$variant data-timepicker-field>
|
||||
<input
|
||||
{{ $inputAttributes }}
|
||||
id="{{ $id }}"
|
||||
@@ -172,7 +172,7 @@
|
||||
data-field-clear
|
||||
@disabled($disabled)
|
||||
>
|
||||
<x-icon name="close" class="size-(--field-icon)" />
|
||||
<x-livewire-material::icon name="close" class="size-(--field-icon)" />
|
||||
</button>
|
||||
@endif
|
||||
|
||||
@@ -185,10 +185,10 @@
|
||||
data-timepicker-open
|
||||
@disabled($disabled)
|
||||
>
|
||||
<x-icon name="schedule" class="size-(--field-icon)" />
|
||||
<x-livewire-material::icon name="schedule" class="size-(--field-icon)" />
|
||||
</button>
|
||||
</x-slot:trailing>
|
||||
</x-field>
|
||||
</x-livewire-material::field>
|
||||
|
||||
@if (filled($name))
|
||||
<input type="hidden" name="{{ $name }}" value="{{ $current }}" x-bind:value="value ?? ''" />
|
||||
@@ -348,14 +348,14 @@
|
||||
|
||||
<div data-timepicker-actions>
|
||||
<span data-timepicker-when="dial">
|
||||
<x-button icon="keyboard" :tooltip="__('Switch to text input mode')" x-on:click="toggleMode()" data-timepicker-mode="input" />
|
||||
<x-livewire-material::button icon="keyboard" :tooltip="__('Switch to text input mode')" x-on:click="toggleMode()" data-timepicker-mode="input" />
|
||||
</span>
|
||||
<span data-timepicker-when="input">
|
||||
<x-button icon="schedule" :tooltip="__('Switch to clock mode')" x-on:click="toggleMode()" data-timepicker-mode="dial" />
|
||||
<x-livewire-material::button icon="schedule" :tooltip="__('Switch to clock mode')" x-on:click="toggleMode()" data-timepicker-mode="dial" />
|
||||
</span>
|
||||
<span data-timepicker-spacer></span>
|
||||
<x-button :label="__('Cancel')" x-on:click="cancel()" data-timepicker-cancel />
|
||||
<x-button :label="__('OK')" x-on:click="confirm()" data-timepicker-confirm />
|
||||
<x-livewire-material::button :label="__('Cancel')" x-on:click="cancel()" data-timepicker-cancel />
|
||||
<x-livewire-material::button :label="__('OK')" x-on:click="confirm()" data-timepicker-confirm />
|
||||
</div>
|
||||
</div>
|
||||
</dialog>
|
||||
|
||||
@@ -47,10 +47,10 @@
|
||||
'text-inverse-warning': current.type === 'warning',
|
||||
'text-inverse-info': current.type === 'info',
|
||||
}">
|
||||
<span x-show="current.type === 'success'"><x-icon name="check_circle" filled class="size-6" /></span>
|
||||
<span x-show="current.type === 'error'"><x-icon name="error" filled class="size-6" /></span>
|
||||
<span x-show="current.type === 'warning'"><x-icon name="warning" filled class="size-6" /></span>
|
||||
<span x-show="current.type === 'info'"><x-icon name="info" filled class="size-6" /></span>
|
||||
<span x-show="current.type === 'success'"><x-livewire-material::icon name="check_circle" filled class="size-6" /></span>
|
||||
<span x-show="current.type === 'error'"><x-livewire-material::icon name="error" filled class="size-6" /></span>
|
||||
<span x-show="current.type === 'warning'"><x-livewire-material::icon name="warning" filled class="size-6" /></span>
|
||||
<span x-show="current.type === 'info'"><x-livewire-material::icon name="info" filled class="size-6" /></span>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
@@ -65,7 +65,7 @@
|
||||
|
||||
<template x-if="current.action || ! current.timeout">
|
||||
<button type="button" class="state-layer focus-ring inline-flex size-10 shrink-0 items-center justify-center rounded-corner-full" aria-label="{{ __('Dismiss') }}" x-on:click="dismiss()">
|
||||
<x-icon name="close" class="size-5" />
|
||||
<x-livewire-material::icon name="close" class="size-5" />
|
||||
</button>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
@@ -36,8 +36,8 @@
|
||||
/>
|
||||
<span data-handle>
|
||||
@if ($icons)
|
||||
<x-icon name="check" class="size-4" data-on />
|
||||
<x-icon name="close" class="size-4" data-off />
|
||||
<x-livewire-material::icon name="check" class="size-4" data-on />
|
||||
<x-livewire-material::icon name="close" class="size-4" data-off />
|
||||
@endif
|
||||
</span>
|
||||
</span>
|
||||
|
||||
@@ -11,6 +11,6 @@
|
||||
@section('message', __('It was open for a while. Refresh it, then try again.'))
|
||||
|
||||
@section('actions')
|
||||
<x-button :link="url()->previous()" :label="__('Refresh the page')" variant="filled" size="md" no-wire-navigate />
|
||||
<x-button :link="url('/')" :label="__('Go home')" variant="text" size="md" no-wire-navigate />
|
||||
<x-livewire-material::button :link="url()->previous()" :label="__('Refresh the page')" variant="filled" size="md" no-wire-navigate />
|
||||
<x-livewire-material::button :link="url('/')" :label="__('Go home')" variant="text" size="md" no-wire-navigate />
|
||||
@endsection
|
||||
|
||||
@@ -18,5 +18,5 @@
|
||||
@section('message', $reason !== '' && $reason !== 'Service Unavailable' ? __($reason) : __('We’re making some improvements. Please check back soon.'))
|
||||
|
||||
@section('actions')
|
||||
<x-button :label="__('Try again')" variant="filled" size="md" onclick="location.reload()" />
|
||||
<x-livewire-material::button :label="__('Try again')" variant="filled" size="md" onclick="location.reload()" />
|
||||
@endsection
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
|
||||
<title>@yield('title') · {{ config('app.name') }}</title>
|
||||
|
||||
<x-theme-script />
|
||||
<x-livewire-material::theme-script />
|
||||
|
||||
@if ($assets !== null)
|
||||
{{ $assets }}
|
||||
@@ -49,7 +49,7 @@
|
||||
<main data-error-page class="mx-auto flex min-h-dvh max-w-xl flex-col items-center justify-center px-6 py-12 text-center">
|
||||
<div data-error-art class="relative grid size-48 shrink-0 place-items-center sm:size-60">
|
||||
<div data-error-shape class="absolute inset-0">
|
||||
<x-shape :name="trim($__env->yieldContent('shape', 'cookie-7'))" class="size-full text-primary-container" />
|
||||
<x-livewire-material::shape :name="trim($__env->yieldContent('shape', 'cookie-7'))" class="size-full text-primary-container" />
|
||||
</div>
|
||||
|
||||
<p data-error-code class="relative type-emphasized-display-lg text-on-primary-container tabular-nums">@yield('code')</p>
|
||||
@@ -71,10 +71,10 @@
|
||||
@hasSection('actions')
|
||||
@yield('actions')
|
||||
@else
|
||||
<x-button :link="url('/')" :label="__('Go home')" variant="filled" size="md" no-wire-navigate />
|
||||
<x-livewire-material::button :link="url('/')" :label="__('Go home')" variant="filled" size="md" no-wire-navigate />
|
||||
|
||||
@if ($back !== null)
|
||||
<x-button :link="$back" :label="__('Go back')" variant="text" size="md" no-wire-navigate />
|
||||
<x-livewire-material::button :link="$back" :label="__('Go back')" variant="text" size="md" no-wire-navigate />
|
||||
@endif
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@@ -4,15 +4,15 @@
|
||||
@if ($paginator->hasPages())
|
||||
<nav role="navigation" aria-label="{{ __('Pagination Navigation') }}" data-pagination class="flex items-center justify-between gap-4">
|
||||
@if ($paginator->onFirstPage())
|
||||
<x-button variant="outlined" icon="chevron_left" :label="__('Previous')" disabled />
|
||||
<x-livewire-material::button variant="outlined" icon="chevron_left" :label="__('Previous')" disabled />
|
||||
@else
|
||||
<x-button variant="outlined" icon="chevron_left" :label="__('Previous')" :link="$paginator->previousPageUrl()" no-wire-navigate rel="prev" />
|
||||
<x-livewire-material::button variant="outlined" icon="chevron_left" :label="__('Previous')" :link="$paginator->previousPageUrl()" no-wire-navigate rel="prev" />
|
||||
@endif
|
||||
|
||||
@if ($paginator->hasMorePages())
|
||||
<x-button variant="outlined" icon-right="chevron_right" :label="__('Next')" :link="$paginator->nextPageUrl()" no-wire-navigate rel="next" />
|
||||
<x-livewire-material::button variant="outlined" icon-right="chevron_right" :label="__('Next')" :link="$paginator->nextPageUrl()" no-wire-navigate rel="next" />
|
||||
@else
|
||||
<x-button variant="outlined" icon-right="chevron_right" :label="__('Next')" disabled />
|
||||
<x-livewire-material::button variant="outlined" icon-right="chevron_right" :label="__('Next')" disabled />
|
||||
@endif
|
||||
</nav>
|
||||
@endif
|
||||
|
||||
@@ -19,11 +19,11 @@
|
||||
<div class="flex items-center gap-1">
|
||||
@if ($paginator->onFirstPage())
|
||||
<span class="{{ $dead }}" aria-disabled="true" aria-label="{{ __('Previous') }}">
|
||||
<x-icon name="chevron_left" class="size-6 rtl:-scale-x-100" />
|
||||
<x-livewire-material::icon name="chevron_left" class="size-6 rtl:-scale-x-100" />
|
||||
</span>
|
||||
@else
|
||||
<a href="{{ $paginator->previousPageUrl() }}" rel="prev" class="{{ $live }}" aria-label="{{ __('Previous') }}">
|
||||
<x-icon name="chevron_left" class="size-6 rtl:-scale-x-100" />
|
||||
<x-livewire-material::icon name="chevron_left" class="size-6 rtl:-scale-x-100" />
|
||||
</a>
|
||||
@endif
|
||||
|
||||
@@ -45,11 +45,11 @@
|
||||
|
||||
@if ($paginator->hasMorePages())
|
||||
<a href="{{ $paginator->nextPageUrl() }}" rel="next" class="{{ $live }}" aria-label="{{ __('Next') }}">
|
||||
<x-icon name="chevron_right" class="size-6 rtl:-scale-x-100" />
|
||||
<x-livewire-material::icon name="chevron_right" class="size-6 rtl:-scale-x-100" />
|
||||
</a>
|
||||
@else
|
||||
<span class="{{ $dead }}" aria-disabled="true" aria-label="{{ __('Next') }}">
|
||||
<x-icon name="chevron_right" class="size-6 rtl:-scale-x-100" />
|
||||
<x-livewire-material::icon name="chevron_right" class="size-6 rtl:-scale-x-100" />
|
||||
</span>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@@ -19,21 +19,21 @@
|
||||
@if ($paginator->hasPages())
|
||||
<nav role="navigation" aria-label="{{ __('Pagination Navigation') }}" data-pagination class="flex items-center justify-between gap-4">
|
||||
@if ($paginator->onFirstPage())
|
||||
<x-button variant="outlined" icon="chevron_left" :label="__('Previous')" disabled />
|
||||
<x-livewire-material::button variant="outlined" icon="chevron_left" :label="__('Previous')" disabled />
|
||||
@elseif ($cursor)
|
||||
@php($previousCursor = $paginator->previousCursor() ?? $paginator->cursor())
|
||||
<x-button variant="outlined" icon="chevron_left" :label="__('Previous')" wire:key="cursor-{{ $paginator->getCursorName() }}-{{ $previousCursor?->encode() }}" wire:click="setPage('{{ $previousCursor?->encode() }}', '{{ $paginator->getCursorName() }}')" x-on:click="{{ $scrollIntoViewJsSnippet }}" wire:loading.attr="disabled" dusk="previousPage" />
|
||||
<x-livewire-material::button variant="outlined" icon="chevron_left" :label="__('Previous')" wire:key="cursor-{{ $paginator->getCursorName() }}-{{ $previousCursor?->encode() }}" wire:click="setPage('{{ $previousCursor?->encode() }}', '{{ $paginator->getCursorName() }}')" x-on:click="{{ $scrollIntoViewJsSnippet }}" wire:loading.attr="disabled" dusk="previousPage" />
|
||||
@else
|
||||
<x-button variant="outlined" icon="chevron_left" :label="__('Previous')" wire:click="previousPage('{{ $paginator->getPageName() }}')" x-on:click="{{ $scrollIntoViewJsSnippet }}" wire:loading.attr="disabled" dusk="previousPage" />
|
||||
<x-livewire-material::button variant="outlined" icon="chevron_left" :label="__('Previous')" wire:click="previousPage('{{ $paginator->getPageName() }}')" x-on:click="{{ $scrollIntoViewJsSnippet }}" wire:loading.attr="disabled" dusk="previousPage" />
|
||||
@endif
|
||||
|
||||
@if (! $paginator->hasMorePages())
|
||||
<x-button variant="outlined" icon-right="chevron_right" :label="__('Next')" disabled />
|
||||
<x-livewire-material::button variant="outlined" icon-right="chevron_right" :label="__('Next')" disabled />
|
||||
@elseif ($cursor)
|
||||
@php($nextCursor = $paginator->nextCursor() ?? $paginator->cursor())
|
||||
<x-button variant="outlined" icon-right="chevron_right" :label="__('Next')" wire:key="cursor-{{ $paginator->getCursorName() }}-{{ $nextCursor?->encode() }}" wire:click="setPage('{{ $nextCursor?->encode() }}', '{{ $paginator->getCursorName() }}')" x-on:click="{{ $scrollIntoViewJsSnippet }}" wire:loading.attr="disabled" dusk="nextPage" />
|
||||
<x-livewire-material::button variant="outlined" icon-right="chevron_right" :label="__('Next')" wire:key="cursor-{{ $paginator->getCursorName() }}-{{ $nextCursor?->encode() }}" wire:click="setPage('{{ $nextCursor?->encode() }}', '{{ $paginator->getCursorName() }}')" x-on:click="{{ $scrollIntoViewJsSnippet }}" wire:loading.attr="disabled" dusk="nextPage" />
|
||||
@else
|
||||
<x-button variant="outlined" icon-right="chevron_right" :label="__('Next')" wire:click="nextPage('{{ $paginator->getPageName() }}')" x-on:click="{{ $scrollIntoViewJsSnippet }}" wire:loading.attr="disabled" dusk="nextPage" />
|
||||
<x-livewire-material::button variant="outlined" icon-right="chevron_right" :label="__('Next')" wire:click="nextPage('{{ $paginator->getPageName() }}')" x-on:click="{{ $scrollIntoViewJsSnippet }}" wire:loading.attr="disabled" dusk="nextPage" />
|
||||
@endif
|
||||
</nav>
|
||||
@endif
|
||||
|
||||
@@ -35,11 +35,11 @@
|
||||
<div class="flex items-center gap-1">
|
||||
@if ($paginator->onFirstPage())
|
||||
<span class="{{ $dead }}" aria-disabled="true" aria-label="{{ __('Previous') }}">
|
||||
<x-icon name="chevron_left" class="size-6 rtl:-scale-x-100" />
|
||||
<x-livewire-material::icon name="chevron_left" class="size-6 rtl:-scale-x-100" />
|
||||
</span>
|
||||
@else
|
||||
<button type="button" wire:click="previousPage('{{ $paginator->getPageName() }}')" x-on:click="{{ $scrollIntoViewJsSnippet }}" wire:loading.attr="disabled" dusk="previousPage{{ $paginator->getPageName() == 'page' ? '' : '.'.$paginator->getPageName() }}" class="{{ $live }}" aria-label="{{ __('Previous') }}">
|
||||
<x-icon name="chevron_left" class="size-6 rtl:-scale-x-100" />
|
||||
<x-livewire-material::icon name="chevron_left" class="size-6 rtl:-scale-x-100" />
|
||||
</button>
|
||||
@endif
|
||||
|
||||
@@ -63,11 +63,11 @@
|
||||
|
||||
@if ($paginator->hasMorePages())
|
||||
<button type="button" wire:click="nextPage('{{ $paginator->getPageName() }}')" x-on:click="{{ $scrollIntoViewJsSnippet }}" wire:loading.attr="disabled" dusk="nextPage{{ $paginator->getPageName() == 'page' ? '' : '.'.$paginator->getPageName() }}" class="{{ $live }}" aria-label="{{ __('Next') }}">
|
||||
<x-icon name="chevron_right" class="size-6 rtl:-scale-x-100" />
|
||||
<x-livewire-material::icon name="chevron_right" class="size-6 rtl:-scale-x-100" />
|
||||
</button>
|
||||
@else
|
||||
<span class="{{ $dead }}" aria-disabled="true" aria-label="{{ __('Next') }}">
|
||||
<x-icon name="chevron_right" class="size-6 rtl:-scale-x-100" />
|
||||
<x-livewire-material::icon name="chevron_right" class="size-6 rtl:-scale-x-100" />
|
||||
</span>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@@ -3,7 +3,22 @@
|
||||
|
||||
@props(['title' => null, 'code', 'stack' => false])
|
||||
|
||||
<div class="space-y-4 rounded-corner-lg bg-surface-container p-4">
|
||||
@php
|
||||
// Written as an application without a prefix writes it; under a configured prefix the tags are
|
||||
// rewritten to it, so what renders and what is shown is what this application would write.
|
||||
$prefix = config('livewire-material.prefix');
|
||||
|
||||
if (filled($prefix)) {
|
||||
$names = collect(glob(\NoNameWeb\LivewireMaterial\LivewireMaterialServiceProvider::componentPath().'/*.blade.php'))
|
||||
->map(fn (string $path): string => preg_quote(basename($path, '.blade.php'), '/'))
|
||||
->sortByDesc(fn (string $name): int => strlen($name))
|
||||
->implode('|');
|
||||
|
||||
$code = preg_replace('/<(\/?)x-('.$names.')(?=[\s\/>])/', '<$1x-'.$prefix.'::$2', $code);
|
||||
}
|
||||
@endphp
|
||||
|
||||
<div @if ($title) id="{{ \NoNameWeb\LivewireMaterial\Showcase\Sections::anchor($title) }}" @endif class="scroll-mt-24 space-y-4 rounded-corner-lg bg-surface-container p-4">
|
||||
@if ($title)
|
||||
<h3 class="type-title-md">{{ $title }}</h3>
|
||||
@endif
|
||||
|
||||
@@ -1,31 +1,36 @@
|
||||
@extends('livewire-material::showcase.layout')
|
||||
|
||||
@section('content')
|
||||
<main class="mx-auto max-w-6xl space-y-16 px-4 py-10">
|
||||
<p class="max-w-3xl type-body-lg text-on-surface-variant">
|
||||
Every token and component, in this application's own scheme. Components appear here as they land.
|
||||
</p>
|
||||
<div class="mx-auto w-full max-w-6xl space-y-12 px-4 pt-4 pb-16 sm:px-6">
|
||||
<div class="max-w-3xl space-y-3">
|
||||
<h1 class="type-headline-lg">Livewire Material</h1>
|
||||
<p class="type-title-lg">Material 3 Expressive for Laravel and Livewire.</p>
|
||||
<p class="type-body-lg text-on-surface-variant">
|
||||
Every token and component, rendered in this application's own scheme and theme. Pick a section in the
|
||||
navigation, search for one above (or press <kbd class="rounded-corner-xs bg-surface-container-highest px-1.5 type-label-md">/</kbd>), or start below.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@include('livewire-material::showcase.sections.colour')
|
||||
@include('livewire-material::showcase.sections.type')
|
||||
@include('livewire-material::showcase.sections.shape')
|
||||
@include('livewire-material::showcase.sections.elevation')
|
||||
@include('livewire-material::showcase.sections.motion')
|
||||
@include('livewire-material::showcase.sections.icons')
|
||||
@include('livewire-material::showcase.sections.buttons')
|
||||
@include('livewire-material::showcase.sections.menus')
|
||||
@include('livewire-material::showcase.sections.communication')
|
||||
@include('livewire-material::showcase.sections.progress')
|
||||
@include('livewire-material::showcase.sections.containment')
|
||||
@include('livewire-material::showcase.sections.carousel')
|
||||
@include('livewire-material::showcase.sections.fields')
|
||||
@include('livewire-material::showcase.sections.chips')
|
||||
@include('livewire-material::showcase.sections.sliders')
|
||||
@include('livewire-material::showcase.sections.pickers')
|
||||
@include('livewire-material::showcase.sections.timepickers')
|
||||
@include('livewire-material::showcase.sections.bars')
|
||||
@include('livewire-material::showcase.sections.navigation')
|
||||
@include('livewire-material::showcase.sections.data')
|
||||
@include('livewire-material::showcase.sections.pages')
|
||||
</main>
|
||||
@foreach (collect($sections)->groupBy('group', preserveKeys: true) as $group => $entries)
|
||||
<section class="space-y-4" aria-labelledby="group-{{ \Illuminate\Support\Str::slug($group) }}">
|
||||
<h2 id="group-{{ \Illuminate\Support\Str::slug($group) }}" class="type-title-lg">{{ $group }}</h2>
|
||||
|
||||
<div class="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
||||
@foreach ($entries as $key => $entry)
|
||||
<x-livewire-material::card variant="outlined" data-list-row wire:key="section-{{ $key }}">
|
||||
<div class="flex items-start gap-4">
|
||||
<span class="grid size-12 shrink-0 place-items-center rounded-corner-lg bg-secondary-container text-on-secondary-container">
|
||||
<x-livewire-material::icon :name="$entry['icon']" />
|
||||
</span>
|
||||
<div class="min-w-0">
|
||||
<a href="{{ route('livewire-material.section', $key) }}" wire:navigate data-list-open class="type-title-md">{{ $entry['title'] }}</a>
|
||||
<p class="mt-1 type-body-md text-on-surface-variant">{{ $entry['description'] }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</x-livewire-material::card>
|
||||
@endforeach
|
||||
</div>
|
||||
</section>
|
||||
@endforeach
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
@@ -1,47 +1,166 @@
|
||||
{{-- The showcase's frame, and the package's own app shell at work: the rail groups every section,
|
||||
collapses and expands from `lg`, and opens as a modal from the app bar's menu button on a phone.
|
||||
Pages move with wire:navigate, so the rail keeps its place and the theme stays. --}}
|
||||
|
||||
@php
|
||||
$sections ??= \NoNameWeb\LivewireMaterial\Showcase\Sections::all();
|
||||
$section ??= null;
|
||||
$title = $section !== null ? $sections[$section]['title'] : 'Livewire Material';
|
||||
|
||||
$destinations = [[
|
||||
'title' => 'Overview',
|
||||
'icon' => 'home',
|
||||
'url' => route('livewire-material.showcase'),
|
||||
'active' => $section === null,
|
||||
'bar' => false,
|
||||
]];
|
||||
|
||||
foreach ($sections as $key => $entry) {
|
||||
$destinations[] = [
|
||||
'title' => $entry['title'],
|
||||
'icon' => $entry['icon'],
|
||||
'url' => route('livewire-material.section', $key),
|
||||
'active' => $key === $section,
|
||||
'section' => $entry['group'],
|
||||
'bar' => false,
|
||||
];
|
||||
}
|
||||
@endphp
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
|
||||
<meta name="robots" content="noindex" />
|
||||
|
||||
<title>@yield('title', 'Livewire Material')</title>
|
||||
<title>{{ $section !== null ? $title.' · Livewire Material' : 'Livewire Material' }}</title>
|
||||
|
||||
<x-theme-script />
|
||||
<x-livewire-material::theme-script />
|
||||
|
||||
@vite(config('livewire-material.showcase.vite'))
|
||||
@livewireStyles
|
||||
</head>
|
||||
<body class="min-h-screen bg-surface font-sans text-on-surface antialiased">
|
||||
<header class="sticky top-0 z-10 border-b border-divider bg-surface-container">
|
||||
<div class="mx-auto flex max-w-6xl items-center gap-x-6 px-4 py-3">
|
||||
<a href="{{ route('livewire-material.showcase') }}" class="shrink-0 type-title-lg max-sm:hidden">Livewire Material</a>
|
||||
<body class="bg-surface font-sans text-on-surface antialiased">
|
||||
<x-livewire-material::app-shell :destinations="$destinations" label="Showcase" rail-width="17rem">
|
||||
<x-slot:brand>
|
||||
<a href="{{ route('livewire-material.showcase') }}" wire:navigate class="block truncate rounded-corner-xs type-title-lg focus-ring">Livewire Material</a>
|
||||
</x-slot:brand>
|
||||
|
||||
<nav class="-my-2 flex min-w-0 flex-1 gap-x-4 overflow-x-auto py-2 whitespace-nowrap type-label-lg text-on-surface-variant [scrollbar-width:none]" aria-label="Sections">
|
||||
@foreach (['colour' => 'Colour', 'type' => 'Type', 'shape' => 'Shape', 'elevation' => 'Elevation', 'motion' => 'Motion', 'icons' => 'Icons', 'buttons' => 'Buttons', 'menus' => 'Menus', 'communication' => 'Communication', 'progress' => 'Progress', 'containment' => 'Containment', 'carousel' => 'Carousel', 'fields' => 'Fields', 'chips' => 'Chips', 'sliders' => 'Sliders', 'pickers' => 'Date pickers', 'timepickers' => 'Time pickers', 'bars' => 'Bars', 'navigation' => 'Navigation', 'data' => 'Data', 'pages' => 'Pages'] as $anchor => $section)
|
||||
<a href="#{{ $anchor }}" class="rounded-corner-xs hover:text-on-surface focus-ring">{{ $section }}</a>
|
||||
@endforeach
|
||||
</nav>
|
||||
<x-slot:top>
|
||||
<x-livewire-material::app-bar variant="search">
|
||||
<x-slot:navigation>
|
||||
<span class="sm:hidden"><x-livewire-material::button icon="menu" tooltip="Open navigation" x-data x-on:click="$store.rail.show()" data-test="showcase-menu" /></span>
|
||||
</x-slot:navigation>
|
||||
|
||||
<div class="ms-auto flex shrink-0 rounded-corner-full border border-outline" role="group" aria-label="Theme" x-data x-cloak>
|
||||
@foreach (['light' => 'Light', 'dark' => 'Dark', 'system' => 'System'] as $choice => $label)
|
||||
<button
|
||||
type="button"
|
||||
data-test="theme-{{ $choice }}"
|
||||
class="state-layer focus-ring px-4 py-1.5 type-label-lg first:rounded-s-corner-full last:rounded-e-corner-full"
|
||||
x-on:click="$store.theme.set('{{ $choice }}')"
|
||||
x-bind:class="$store.theme.choice === '{{ $choice }}' && 'bg-secondary-container text-on-secondary-container'"
|
||||
x-bind:aria-pressed="($store.theme.choice === '{{ $choice }}').toString()"
|
||||
>{{ $label }}</button>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
{{-- The search looks through every section, example and component: the index is
|
||||
fetched on first focus, and / or Ctrl+K (⌘K) reaches it from anywhere. Its names
|
||||
keep clear of <x-search>'s own (`results`, `open`), which the slot also sees. --}}
|
||||
<div
|
||||
class="mx-auto w-full max-w-2xl"
|
||||
x-data="{
|
||||
query: '',
|
||||
entries: null,
|
||||
async load() {
|
||||
this.entries ??= await (await fetch(@js(route('livewire-material.search', [], false)))).json();
|
||||
},
|
||||
get matches() {
|
||||
const words = this.query.toLowerCase().split(/\s+/).filter(Boolean);
|
||||
if (! this.entries || words.length === 0) return [];
|
||||
const phrase = words.join(' ');
|
||||
return this.entries
|
||||
.map((entry) => {
|
||||
const title = entry.title.toLowerCase().replace(/[<>]/g, '');
|
||||
const text = `${title} ${entry.context} ${entry.kind} ${entry.keywords}`.toLowerCase();
|
||||
if (! words.every((word) => text.includes(word))) return null;
|
||||
const rank = title === phrase ? 0 : title.startsWith(phrase) ? 1 : title.includes(phrase) ? 2 : 3;
|
||||
return { ...entry, rank };
|
||||
})
|
||||
.filter(Boolean)
|
||||
.sort((a, b) => a.rank - b.rank)
|
||||
.slice(0, 30);
|
||||
},
|
||||
go(event, result) {
|
||||
if (! result || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey || (event.button ?? 0) !== 0) return;
|
||||
event.preventDefault();
|
||||
const url = new URL(result.url, window.location.href);
|
||||
const search = this.$root.querySelector('[data-search]');
|
||||
this.query = '';
|
||||
if (search) window.Alpine.$data(search).close();
|
||||
if (url.pathname === window.location.pathname) {
|
||||
window.location.hash = url.hash;
|
||||
document.getElementById(url.hash.slice(1))?.scrollIntoView({ block: 'start' });
|
||||
} else {
|
||||
window.Livewire.navigate(url.pathname + url.hash);
|
||||
}
|
||||
},
|
||||
shortcut(event) {
|
||||
const typing = event.target.closest('input, textarea, select, [contenteditable]');
|
||||
if ((event.key === '/' && ! typing) || (event.key.toLowerCase() === 'k' && (event.metaKey || event.ctrlKey))) {
|
||||
event.preventDefault();
|
||||
document.getElementById('showcase-search').focus();
|
||||
}
|
||||
},
|
||||
}"
|
||||
x-on:keydown.window="shortcut($event)"
|
||||
>
|
||||
<x-livewire-material::search
|
||||
id="showcase-search"
|
||||
placeholder="Search components, examples and sections"
|
||||
x-model="query"
|
||||
x-on:focus.once="load()"
|
||||
x-on:keydown.enter.prevent="go($event, matches[0])"
|
||||
>
|
||||
<p x-show="query.trim() === ''" class="px-4 py-3 type-body-md text-on-surface-variant">
|
||||
Type a component (<code>datepicker</code>), an example or a section. Press <kbd class="rounded-corner-xs bg-surface-container-highest px-1.5 type-label-md">/</kbd> to search from anywhere.
|
||||
</p>
|
||||
<p x-cloak x-show="query.trim() !== '' && entries !== null && matches.length === 0" class="px-4 py-3 type-body-md text-on-surface-variant">
|
||||
Nothing matches “<span x-text="query.trim()"></span>”.
|
||||
</p>
|
||||
<template x-for="result in matches" x-bind:key="result.url + result.title">
|
||||
<a
|
||||
x-bind:href="result.url"
|
||||
x-on:click="go($event, result)"
|
||||
class="state-layer focus-ring flex min-h-14 flex-col justify-center px-4 py-2 outline-none"
|
||||
data-showcase-result
|
||||
>
|
||||
<span class="truncate type-body-lg text-on-surface" x-text="result.title"></span>
|
||||
<span class="truncate type-body-sm text-on-surface-variant" x-text="`${result.kind} · ${result.context}`"></span>
|
||||
</a>
|
||||
</template>
|
||||
</x-livewire-material::search>
|
||||
</div>
|
||||
|
||||
@yield('content')
|
||||
<x-slot:actions>
|
||||
<span class="ms-2 me-3 max-md:hidden"><x-livewire-material::theme-toggle mode="picker" /></span>
|
||||
<span class="md:hidden"><x-livewire-material::theme-toggle mode="cycle" /></span>
|
||||
</x-slot:actions>
|
||||
</x-livewire-material::app-bar>
|
||||
</x-slot:top>
|
||||
|
||||
<x-toast />
|
||||
@yield('content')
|
||||
</x-livewire-material::app-shell>
|
||||
|
||||
@livewireScripts
|
||||
|
||||
{{-- wire:navigate keeps the URL's hash but not the scroll to it: a search result that opens an
|
||||
example on another page lands on the example. --}}
|
||||
<script data-navigate-once>
|
||||
document.addEventListener('livewire:navigated', () => {
|
||||
const target = () => (window.location.hash.length > 1 ? document.getElementById(decodeURIComponent(window.location.hash.slice(1))) : null);
|
||||
const land = () => target()?.scrollIntoView({ block: 'start' });
|
||||
|
||||
// After the swap's own scroll to the top and the page transition, and once more when
|
||||
// the page's components have settled their size.
|
||||
requestAnimationFrame(() => requestAnimationFrame(land));
|
||||
setTimeout(() => {
|
||||
const box = target()?.getBoundingClientRect();
|
||||
|
||||
if (box && (box.top < 0 || box.top > window.innerHeight / 2)) {
|
||||
land();
|
||||
}
|
||||
}, 400);
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
{{-- One section of the showcase on a page of its own, with the way on to the next. The page's
|
||||
heading names the section, so the section's own heading is only for a screen reader. --}}
|
||||
|
||||
@extends('livewire-material::showcase.layout')
|
||||
|
||||
@php
|
||||
$keys = array_keys($sections);
|
||||
$at = array_search($section, $keys, true);
|
||||
$previous = $keys[$at - 1] ?? null;
|
||||
$next = $keys[$at + 1] ?? null;
|
||||
@endphp
|
||||
|
||||
@section('content')
|
||||
<div class="mx-auto w-full max-w-6xl px-4 pt-4 pb-16 sm:px-6">
|
||||
<header class="mb-6 space-y-1">
|
||||
<p class="type-label-lg text-on-surface-variant">{{ $sections[$section]['group'] }}</p>
|
||||
<h1 class="type-headline-lg">{{ $sections[$section]['title'] }}</h1>
|
||||
</header>
|
||||
|
||||
<div class="space-y-16 [&>section>h2]:sr-only">
|
||||
@include('livewire-material::showcase.sections.'.$section)
|
||||
|
||||
<nav aria-label="Sections" class="flex flex-wrap items-center justify-between gap-4 border-t border-divider pt-6">
|
||||
@if ($previous)
|
||||
<x-livewire-material::button variant="text" icon="arrow_back" :label="$sections[$previous]['title']" :link="route('livewire-material.section', $previous)" data-test="previous-section" />
|
||||
@else
|
||||
<x-livewire-material::button variant="text" icon="arrow_back" label="Overview" :link="route('livewire-material.showcase')" data-test="previous-section" />
|
||||
@endif
|
||||
|
||||
@if ($next)
|
||||
<x-livewire-material::button variant="tonal" icon-right="arrow_forward" :label="$sections[$next]['title']" :link="route('livewire-material.section', $next)" data-test="next-section" />
|
||||
@endif
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
@@ -76,7 +76,7 @@
|
||||
<x-button icon="format_underlined" aria-label="Underline" variant="tonal" :selected="false" />
|
||||
</x-button-group>
|
||||
BLADE,
|
||||
'A choice as a connected group (<x-group>)' => <<<'BLADE'
|
||||
'A choice as a connected group' => <<<'BLADE'
|
||||
<div x-data="{ theme: 'system', days: ['mon'] }" class="grid w-full gap-6 md:grid-cols-2">
|
||||
<x-group label="Theme" name="showcase-theme" x-model="theme" :options="[
|
||||
['id' => 'light', 'name' => 'Light', 'icon' => 'light_mode'],
|
||||
|
||||
@@ -15,6 +15,13 @@
|
||||
<x-button label="With an action" variant="tonal" x-on:click="materialToast('Share deleted', { action: { label: 'Undo', handler: () => materialToast('Share restored', { type: 'info' }) } })" />
|
||||
<x-button label="Until dismissed" variant="tonal" x-on:click="materialToast('Your storage is almost full', { type: 'warning', timeout: 0 })" />
|
||||
BLADE,
|
||||
'Plain tooltips' => <<<'BLADE'
|
||||
<x-button icon="content_copy" tooltip="Copy link" />
|
||||
<x-button icon="qr_code_2" tooltip-bottom="Show QR code" />
|
||||
<x-tooltip text="Expires in 3 days" side="right">
|
||||
<span tabindex="0" class="focus-ring rounded-corner-xs type-body-md text-on-surface-variant">holiday-photos.zip</span>
|
||||
</x-tooltip>
|
||||
BLADE,
|
||||
'Rich tooltips' => <<<'BLADE'
|
||||
<x-rich-tooltip title="Expiry" text="Recipients lose access after this time. Admins can change the longest time allowed.">
|
||||
<x-button icon="help" aria-label="About expiry" />
|
||||
@@ -59,7 +66,7 @@
|
||||
|
||||
<p class="max-w-3xl type-body-md text-on-surface-variant">
|
||||
<code><x-badge></code>, <code><x-toast></code> (the snackbar host, fed by the <code>Toasts</code> concern or <code>materialToast()</code>),
|
||||
<code><x-rich-tooltip></code>, <code><x-alert></code>, <code><x-stat></code> and <code><x-empty-state></code>.
|
||||
<code><x-tooltip></code>, <code><x-rich-tooltip></code>, <code><x-alert></code>, <code><x-stat></code> and <code><x-empty-state></code>.
|
||||
</p>
|
||||
|
||||
@foreach ($examples as $title => $code)
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
|
||||
<div class="flex flex-wrap items-center gap-4">
|
||||
<label class="flex min-w-72 flex-1 items-center gap-2 rounded-corner-xs border border-outline px-3 py-2 focus-within:outline-3 focus-within:outline-secondary">
|
||||
<x-icon name="search" class="size-5 text-on-surface-variant" />
|
||||
<x-livewire-material::icon name="search" class="size-5 text-on-surface-variant" />
|
||||
<input type="search" x-model.debounce.150ms="query" placeholder="Search {{ number_format(count(\NoNameWeb\LivewireMaterial\Support\SvgFile::symbolNames())) }} symbols" class="w-full bg-transparent type-body-lg outline-none" />
|
||||
</label>
|
||||
|
||||
|
||||
@@ -8,9 +8,9 @@
|
||||
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
@foreach (['401', '402', '403', '404', '419', '429', '500', '503'] as $code)
|
||||
<x-button :link="route('livewire-material.error', $code)" :label="$code" variant="tonal" no-wire-navigate />
|
||||
<x-livewire-material::button :link="route('livewire-material.error', $code)" :label="$code" variant="tonal" no-wire-navigate />
|
||||
@endforeach
|
||||
|
||||
<x-button :link="route('livewire-material.mail')" label="Sample mail" icon="mail" variant="outlined" no-wire-navigate />
|
||||
<x-livewire-material::button :link="route('livewire-material.mail')" label="Sample mail" icon="mail" variant="outlined" no-wire-navigate />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
@php
|
||||
$examples = [
|
||||
'docked' => <<<'BLADE'
|
||||
'Docked' => <<<'BLADE'
|
||||
<div class="grid w-full gap-6 md:grid-cols-2" x-data="{ expires: '{{ now()->addWeek()->format('Y-m-d') }}', starts: null }">
|
||||
<div class="grid content-start gap-4">
|
||||
<x-datepicker label="Expires on" x-model="expires" hint="Type a date or pick one" />
|
||||
<x-datepicker label="Expires on" clearable x-model="expires" hint="Type a date or pick one" />
|
||||
<p class="type-body-sm text-on-surface-variant">Bound value: <code x-text="JSON.stringify(expires)"></code></p>
|
||||
</div>
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
</div>
|
||||
</div>
|
||||
BLADE,
|
||||
'modal and modal input' => <<<'BLADE'
|
||||
'Modal and modal input' => <<<'BLADE'
|
||||
<div class="grid w-full gap-6 md:grid-cols-2" x-data="{ birthday: '1990-05-17', delivery: null }">
|
||||
<div class="grid content-start gap-4">
|
||||
<x-datepicker label="Birthday" mode="modal" x-model="birthday" :max="now()" />
|
||||
@@ -26,7 +26,7 @@
|
||||
</div>
|
||||
</div>
|
||||
BLADE,
|
||||
'range' => <<<'BLADE'
|
||||
'Range' => <<<'BLADE'
|
||||
<div class="grid w-full gap-6 md:grid-cols-2" x-data="{ trip: { start: '{{ now()->addDays(3)->format('Y-m-d') }}', end: '{{ now()->addDays(9)->format('Y-m-d') }}' }, leave: { start: null, end: null } }">
|
||||
<div class="grid content-start gap-4">
|
||||
<x-datepicker label="Trip" range x-model="trip" />
|
||||
@@ -39,7 +39,7 @@
|
||||
</div>
|
||||
</div>
|
||||
BLADE,
|
||||
'limits, errors and states' => <<<'BLADE'
|
||||
'Limits, errors and states' => <<<'BLADE'
|
||||
<div class="grid w-full gap-6 md:grid-cols-2">
|
||||
<div class="grid content-start gap-4">
|
||||
<x-datepicker label="Within the next 30 days" :min="now()" :max="now()->addDays(30)" hint="Days outside are disabled" />
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<div class="grid grid-cols-3 gap-4 sm:grid-cols-5 lg:grid-cols-7">
|
||||
@foreach (\NoNameWeb\LivewireMaterial\Support\SvgFile::shapeNames() as $shape)
|
||||
<div class="space-y-2 text-center">
|
||||
<x-shape :name="$shape" class="mx-auto size-20 text-secondary-container" />
|
||||
<x-livewire-material::shape :name="$shape" class="mx-auto size-20 text-secondary-container" />
|
||||
<code class="type-label-md text-on-surface-variant">{{ $shape }}</code>
|
||||
</div>
|
||||
@endforeach
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
@php
|
||||
$examples = [
|
||||
'Time pickers: 12 and 24 hours, outlined and filled' => <<<'BLADE'
|
||||
'12 and 24 hours, outlined and filled' => <<<'BLADE'
|
||||
<div class="grid w-full gap-6 md:grid-cols-2">
|
||||
<div class="grid content-start gap-4">
|
||||
<x-timepicker label="Meeting starts" value="09:30" hint="The locale's clock" />
|
||||
@@ -15,7 +15,7 @@
|
||||
</div>
|
||||
</div>
|
||||
BLADE,
|
||||
'Time pickers: steps and limits' => <<<'BLADE'
|
||||
'Steps and limits' => <<<'BLADE'
|
||||
<div class="grid w-full gap-6 md:grid-cols-2">
|
||||
<div class="grid content-start gap-4">
|
||||
<x-timepicker label="Appointment" value="10:00" format="24" step="15" min="08:00" max="17:30" hint="Quarter hours from 08:00 to 17:30" />
|
||||
|
||||
@@ -33,54 +33,54 @@
|
||||
|
||||
<title>{{ $current['title'] }} · App shell · Livewire Material</title>
|
||||
|
||||
<x-theme-script />
|
||||
<x-livewire-material::theme-script />
|
||||
|
||||
@vite(config('livewire-material.showcase.vite'))
|
||||
@livewireStyles
|
||||
</head>
|
||||
<body class="bg-surface font-sans text-on-surface antialiased">
|
||||
<x-app-shell :destinations="$destinations">
|
||||
<x-livewire-material::app-shell :destinations="$destinations">
|
||||
<x-slot:brand>
|
||||
<a href="{{ route('livewire-material.showcase') }}" class="block truncate rounded-corner-xs type-title-lg focus-ring">Livewire Material</a>
|
||||
</x-slot:brand>
|
||||
|
||||
<x-slot:rail-header>
|
||||
<span class="rail-collapsed:hidden"><x-fab label="Compose" icon="edit" x-on:click="materialToast('Compose opens here')" /></span>
|
||||
<span class="hidden rail-collapsed:inline-flex"><x-fab icon="edit" tooltip-right="Compose" x-on:click="materialToast('Compose opens here')" /></span>
|
||||
<span class="rail-collapsed:hidden"><x-livewire-material::fab label="Compose" icon="edit" x-on:click="materialToast('Compose opens here')" /></span>
|
||||
<span class="hidden rail-collapsed:inline-flex"><x-livewire-material::fab icon="edit" tooltip-right="Compose" x-on:click="materialToast('Compose opens here')" /></span>
|
||||
</x-slot:rail-header>
|
||||
|
||||
<x-slot:rail-footer>
|
||||
<x-navigation-rail-item label="Settings" icon="settings" link="#settings" no-wire-navigate />
|
||||
<x-livewire-material::navigation-rail-item label="Settings" icon="settings" link="#settings" no-wire-navigate />
|
||||
</x-slot:rail-footer>
|
||||
|
||||
<x-slot:actions>
|
||||
<x-button icon="dark_mode" tooltip-right="Switch theme" x-data x-on:click="$store.theme.toggle()" />
|
||||
<x-button icon="help" tooltip-right="Help" x-on:click="materialToast('Help opens here')" />
|
||||
<x-livewire-material::button icon="dark_mode" tooltip-right="Switch theme" x-data x-on:click="$store.theme.toggle()" />
|
||||
<x-livewire-material::button icon="help" tooltip-right="Help" x-on:click="materialToast('Help opens here')" />
|
||||
</x-slot:actions>
|
||||
|
||||
<x-slot:top>
|
||||
<header class="sticky top-0 z-20 flex h-16 items-center gap-1 bg-surface px-1 pt-[env(safe-area-inset-top)] sm:px-4">
|
||||
<span class="sm:hidden"><x-button icon="menu" tooltip="Open navigation" x-data x-on:click="$store.rail.show()" data-test="shell-menu" /></span>
|
||||
<span class="sm:hidden"><x-livewire-material::button icon="menu" tooltip="Open navigation" x-data x-on:click="$store.rail.show()" data-test="shell-menu" /></span>
|
||||
<h1 class="min-w-0 flex-1 truncate px-3 type-title-lg sm:px-0">{{ $current['title'] }}</h1>
|
||||
<x-button icon="search" tooltip="Search" />
|
||||
<x-livewire-material::button icon="search" tooltip="Search" />
|
||||
</header>
|
||||
</x-slot:top>
|
||||
|
||||
<div class="mx-auto w-full max-w-5xl space-y-4 px-4 pb-6 sm:px-6">
|
||||
<p class="type-body-md text-on-surface-variant" data-test="shell-page">This is the {{ strtolower($current['title']) }} page.</p>
|
||||
|
||||
<x-list segmented>
|
||||
<x-livewire-material::list segmented>
|
||||
@foreach (range(1, 14) as $index)
|
||||
<x-list-item :title="$current['title'].' item '.$index" description="A line of supporting text for this item" :icon="$current['icon']" trailing="{{ $index }}h" wire:key="item-{{ $index }}" />
|
||||
<x-livewire-material::list-item :title="$current['title'].' item '.$index" description="A line of supporting text for this item" :icon="$current['icon']" trailing="{{ $index }}h" wire:key="item-{{ $index }}" />
|
||||
@endforeach
|
||||
</x-list>
|
||||
</x-livewire-material::list>
|
||||
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<x-button label="Show a toast" variant="tonal" x-data x-on:click="materialToast('Moved to archive', { action: { label: 'Undo', handler: () => {} } })" />
|
||||
<x-button label="Back to the showcase" link="{{ route('livewire-material.showcase') }}#navigation" no-wire-navigate />
|
||||
<x-livewire-material::button label="Show a toast" variant="tonal" x-data x-on:click="materialToast('Moved to archive', { action: { label: 'Undo', handler: () => {} } })" />
|
||||
<x-livewire-material::button label="Back to the showcase" link="{{ route('livewire-material.section', 'navigation') }}" no-wire-navigate />
|
||||
</div>
|
||||
</div>
|
||||
</x-app-shell>
|
||||
</x-livewire-material::app-shell>
|
||||
|
||||
@livewireScripts
|
||||
</body>
|
||||
|
||||
+9
-1
@@ -1,10 +1,12 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use NoNameWeb\LivewireMaterial\Http\Controllers\ShowcaseController;
|
||||
use NoNameWeb\LivewireMaterial\Http\Controllers\ShowcasePageController;
|
||||
use NoNameWeb\LivewireMaterial\Http\Controllers\ShowcaseSymbolController;
|
||||
use NoNameWeb\LivewireMaterial\Showcase\Sections;
|
||||
|
||||
Route::view('/', 'livewire-material::showcase.index')->name('showcase');
|
||||
Route::get('/', [ShowcaseController::class, 'index'])->name('showcase');
|
||||
|
||||
Route::get('symbols.json', [ShowcaseSymbolController::class, 'index'])->name('symbols');
|
||||
Route::get('symbols/{style}/{name}.svg', [ShowcaseSymbolController::class, 'show'])->name('symbol');
|
||||
@@ -17,3 +19,9 @@ Route::get('errors/{code}', [ShowcasePageController::class, 'error'])
|
||||
->whereIn('code', ['401', '402', '403', '404', '419', '429', '500', '503'])
|
||||
->name('error');
|
||||
Route::get('mail', [ShowcasePageController::class, 'mail'])->name('mail');
|
||||
|
||||
Route::get('search.json', [ShowcaseController::class, 'search'])->name('search');
|
||||
|
||||
Route::get('{section}', [ShowcaseController::class, 'section'])
|
||||
->whereIn('section', array_keys(Sections::all()))
|
||||
->name('section');
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace NoNameWeb\LivewireMaterial\Http\Controllers;
|
||||
|
||||
use Illuminate\Contracts\View\View;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use NoNameWeb\LivewireMaterial\Showcase\Sections;
|
||||
|
||||
/**
|
||||
* The showcase: an overview of every section, and a page for each.
|
||||
*/
|
||||
class ShowcaseController
|
||||
{
|
||||
public function index(): View
|
||||
{
|
||||
return view('livewire-material::showcase.index', ['sections' => Sections::all()]);
|
||||
}
|
||||
|
||||
public function section(string $section): View
|
||||
{
|
||||
return view('livewire-material::showcase.section', [
|
||||
'sections' => Sections::all(),
|
||||
'section' => $section,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* The search's index, fetched when the search is first focused rather than written into every page.
|
||||
*/
|
||||
public function search(): JsonResponse
|
||||
{
|
||||
return response()->json(Sections::index());
|
||||
}
|
||||
}
|
||||
@@ -47,6 +47,15 @@ class LivewireMaterialServiceProvider extends ServiceProvider
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The anonymous components. Blade names their view namespace after a hash of this string, and
|
||||
* compiled views keep that name, so it stays written the way it always was.
|
||||
*/
|
||||
public static function componentPath(): string
|
||||
{
|
||||
return __DIR__.'/../resources/views/components';
|
||||
}
|
||||
|
||||
/**
|
||||
* The error-view root: a folder holding only `errors/`.
|
||||
*/
|
||||
@@ -105,11 +114,15 @@ class LivewireMaterialServiceProvider extends ServiceProvider
|
||||
|
||||
/**
|
||||
* Register the components as anonymous Blade components under the configured prefix.
|
||||
*
|
||||
* The package's own views never go through this registration: they write
|
||||
* `<x-livewire-material::button>`, which resolves through the view namespace whatever the
|
||||
* prefix, and which an application's own `<x-button>` cannot shadow.
|
||||
*/
|
||||
protected function registerComponents(): void
|
||||
{
|
||||
Blade::anonymousComponentPath(
|
||||
__DIR__.'/../resources/views/components',
|
||||
static::componentPath(),
|
||||
filled(config('livewire-material.prefix')) ? config('livewire-material.prefix') : null,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
<?php
|
||||
|
||||
namespace NoNameWeb\LivewireMaterial\Showcase;
|
||||
|
||||
use Illuminate\Support\Str;
|
||||
use NoNameWeb\LivewireMaterial\LivewireMaterialServiceProvider;
|
||||
|
||||
/**
|
||||
* The showcase's pages: one per section, grouped as the rail groups them.
|
||||
*/
|
||||
class Sections
|
||||
{
|
||||
/**
|
||||
* What the showcase's search looks through: every section, every example in it (linked to the
|
||||
* example's anchor), and every component, linked to the first section that introduces it.
|
||||
*
|
||||
* @return list<array{title: string, kind: string, context: string, url: string, keywords: string}>
|
||||
*/
|
||||
public static function index(): array
|
||||
{
|
||||
$sections = static::all();
|
||||
$components = collect(glob(LivewireMaterialServiceProvider::componentPath().'/*.blade.php'))
|
||||
->map(fn (string $path): string => basename($path, '.blade.php'))
|
||||
->all();
|
||||
$sources = collect($sections)->map(fn (array $section, string $key): string => (string) file_get_contents(static::path($key)));
|
||||
|
||||
$entries = [];
|
||||
$homes = [];
|
||||
|
||||
foreach ($sections as $key => $section) {
|
||||
$url = route('livewire-material.section', $key, false);
|
||||
|
||||
$entries[] = ['title' => $section['title'], 'kind' => 'Section', 'context' => $section['group'], 'url' => $url, 'keywords' => $section['description']];
|
||||
|
||||
preg_match_all("/^\\s*'((?:[^'\\\\]|\\\\.)*)'\\s*=>\\s*<<<'BLADE'/m", $sources[$key], $examples);
|
||||
|
||||
foreach ($examples[1] as $title) {
|
||||
$title = stripslashes($title);
|
||||
$entries[] = ['title' => $title, 'kind' => 'Example', 'context' => $section['title'], 'url' => $url.'#'.static::anchor($title), 'keywords' => ''];
|
||||
}
|
||||
}
|
||||
|
||||
// A section that names a component in its introduction is its home; otherwise the first that uses it.
|
||||
foreach (['/<x-([a-z0-9-]+)>/', '/<x-(?:livewire-material::)?([a-z0-9-]+)(?=[\\s\\/>])/'] as $pattern) {
|
||||
foreach ($sources as $key => $source) {
|
||||
preg_match_all($pattern, $source, $tags);
|
||||
|
||||
foreach ($tags[1] as $tag) {
|
||||
if (in_array($tag, $components, true)) {
|
||||
$homes[$tag] ??= $key;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($homes as $tag => $key) {
|
||||
$entries[] = ['title' => "<x-{$tag}>", 'kind' => 'Component', 'context' => $sections[$key]['title'], 'url' => route('livewire-material.section', $key, false), 'keywords' => str_replace('-', ' ', $tag)];
|
||||
}
|
||||
|
||||
return $entries;
|
||||
}
|
||||
|
||||
/**
|
||||
* The anchor of an example on its section's page.
|
||||
*/
|
||||
public static function anchor(string $title): string
|
||||
{
|
||||
return 'example-'.Str::slug($title);
|
||||
}
|
||||
|
||||
/**
|
||||
* A section's view file.
|
||||
*/
|
||||
public static function path(string $key): string
|
||||
{
|
||||
return dirname(__DIR__, 2).'/resources/views/showcase/sections/'.$key.'.blade.php';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, array{title: string, icon: string, group: string, description: string}>
|
||||
*/
|
||||
public static function all(): array
|
||||
{
|
||||
return [
|
||||
'colour' => ['title' => 'Colour', 'icon' => 'palette', 'group' => 'Foundations', 'description' => 'Every colour role of the generated scheme, light and dark.'],
|
||||
'type' => ['title' => 'Type', 'icon' => 'text_fields', 'group' => 'Foundations', 'description' => 'The typescale, emphasized styles included, in Google Sans Flex.'],
|
||||
'shape' => ['title' => 'Shape', 'icon' => 'interests', 'group' => 'Foundations', 'description' => 'Corner radii and the 35 M3 Expressive shapes.'],
|
||||
'elevation' => ['title' => 'Elevation', 'icon' => 'layers', 'group' => 'Foundations', 'description' => 'Shadows for what floats over content.'],
|
||||
'motion' => ['title' => 'Motion', 'icon' => 'animation', 'group' => 'Foundations', 'description' => 'Spatial springs and effects easings.'],
|
||||
'icons' => ['title' => 'Icons', 'icon' => 'emoji_symbols', 'group' => 'Foundations', 'description' => 'Every Material Symbol, searchable, outlined and filled.'],
|
||||
'buttons' => ['title' => 'Buttons', 'icon' => 'smart_button', 'group' => 'Actions', 'description' => 'Buttons, icon buttons, groups, split buttons and FABs.'],
|
||||
'menus' => ['title' => 'Menus', 'icon' => 'menu_open', 'group' => 'Actions', 'description' => 'Menus with items, groups, choices and shortcuts.'],
|
||||
'chips' => ['title' => 'Chips', 'icon' => 'sell', 'group' => 'Actions', 'description' => 'Assist, filter, input and suggestion chips.'],
|
||||
'communication' => ['title' => 'Communication', 'icon' => 'notifications', 'group' => 'Communication', 'description' => 'Badges, snackbars, tooltips, alerts, stats and empty states.'],
|
||||
'progress' => ['title' => 'Progress', 'icon' => 'progress_activity', 'group' => 'Communication', 'description' => 'Linear, circular and wavy progress, and the loading indicator.'],
|
||||
'containment' => ['title' => 'Containment', 'icon' => 'web_asset', 'group' => 'Containment', 'description' => 'Cards, lists, dividers, dialogs and sheets.'],
|
||||
'carousel' => ['title' => 'Carousel', 'icon' => 'view_carousel', 'group' => 'Containment', 'description' => 'Multi-browse, hero, uncontained and full-screen carousels.'],
|
||||
'fields' => ['title' => 'Text fields', 'icon' => 'edit_note', 'group' => 'Inputs', 'description' => 'Text fields, selects, checkboxes, radios, switches, choices and search.'],
|
||||
'sliders' => ['title' => 'Sliders', 'icon' => 'tune', 'group' => 'Inputs', 'description' => 'Standard, centered and range sliders in five sizes.'],
|
||||
'pickers' => ['title' => 'Date pickers', 'icon' => 'calendar_month', 'group' => 'Inputs', 'description' => 'Docked, modal and input date pickers, single and range.'],
|
||||
'timepickers' => ['title' => 'Time pickers', 'icon' => 'schedule', 'group' => 'Inputs', 'description' => 'The dial and input time picker.'],
|
||||
'bars' => ['title' => 'App bars and tabs', 'icon' => 'toolbar', 'group' => 'Navigation', 'description' => 'Top app bars, toolbars, tabs, section navigation and the account menu.'],
|
||||
'navigation' => ['title' => 'Navigation', 'icon' => 'explore', 'group' => 'Navigation', 'description' => 'The navigation bar, the navigation rail and the app shell.'],
|
||||
'data' => ['title' => 'Data', 'icon' => 'table_chart', 'group' => 'Data and pages', 'description' => 'Data tables, sort headers and pagination.'],
|
||||
'pages' => ['title' => 'Error pages and mail', 'icon' => 'page_info', 'group' => 'Data and pages', 'description' => 'The HTTP error pages and the Markdown mail theme.'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -2,9 +2,9 @@
|
||||
|
||||
const MORE = '#menus [aria-label="More"]';
|
||||
|
||||
function showcase()
|
||||
function showcase(string $section = 'buttons')
|
||||
{
|
||||
return visit('/material')->waitForEvent('networkidle')
|
||||
return visit("/material/{$section}")->waitForEvent('networkidle')
|
||||
->assertScript("document.readyState === 'complete' && typeof window.Alpine !== 'undefined' && typeof window.Livewire !== 'undefined'");
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ function focused(string $expression): string
|
||||
}
|
||||
|
||||
it('opens a menu on the first item and walks it with the keyboard', function () {
|
||||
$page = showcase()
|
||||
$page = showcase('menus')
|
||||
->assertNoJavaScriptErrors()
|
||||
->click(MORE)
|
||||
->assertAttribute(MORE, 'aria-expanded', 'true')
|
||||
@@ -38,7 +38,7 @@ it('opens a menu on the first item and walks it with the keyboard', function ()
|
||||
});
|
||||
|
||||
it('opens a menu from the keyboard, on the last item with ArrowUp', function () {
|
||||
$page = showcase();
|
||||
$page = showcase('menus');
|
||||
|
||||
$page->script("document.querySelector('".MORE."').focus()");
|
||||
|
||||
@@ -50,13 +50,13 @@ it('opens a menu from the keyboard, on the last item with ArrowUp', function ()
|
||||
it('opens a menu the moment the page can be used', function () {
|
||||
// The guard against a light-dismiss press reopening the menu once measured from the page's
|
||||
// time origin, and swallowed every click in the first quarter second.
|
||||
visit('/material')
|
||||
visit('/material/menus')
|
||||
->click(MORE)
|
||||
->assertAttribute(MORE, 'aria-expanded', 'true');
|
||||
});
|
||||
|
||||
it('closes a menu when an item is chosen, but not one that keeps it open', function () {
|
||||
$page = showcase();
|
||||
$page = showcase('menus');
|
||||
|
||||
$page->click(MORE)
|
||||
->click('#menus [role="menuitem"]:has-text("Download")')
|
||||
@@ -76,7 +76,7 @@ it('shows a tooltip on keyboard focus and hides it on Escape', function () {
|
||||
// Firefox only counts a scripted focus as :focus-visible after one. Then focused directly
|
||||
// rather than tabbed to: WebKit, like Safari on macOS, leaves buttons out of the Tab order
|
||||
// unless full keyboard access is on.
|
||||
$page->keys('body', 'Tab');
|
||||
$page->keys('#content', 'Tab');
|
||||
$page->script("document.querySelector('#buttons [aria-label=\"Tonal\"]').focus()");
|
||||
$page->assertScript(focused("getAttribute('aria-label') === 'Tonal'"))
|
||||
->assertScript("{$tooltip}.matches(':popover-open')");
|
||||
|
||||
@@ -64,7 +64,7 @@ const FIRST_SCROLLER = '#carousel [role="region"] >> nth=0';
|
||||
|
||||
function carouselShowcase(array $options = [])
|
||||
{
|
||||
return visit('/material', $options)
|
||||
return visit('/material/carousel', $options)
|
||||
->waitForEvent('networkidle')
|
||||
->assertScript("typeof window.Alpine !== 'undefined'");
|
||||
}
|
||||
|
||||
@@ -74,7 +74,7 @@ function chipProbe()
|
||||
|
||||
function chipShowcase()
|
||||
{
|
||||
return visit('/material')->waitForEvent('networkidle')
|
||||
return visit('/material/chips')->waitForEvent('networkidle')
|
||||
->assertScript("document.readyState === 'complete' && typeof window.Alpine !== 'undefined' && typeof window.Livewire !== 'undefined'");
|
||||
}
|
||||
|
||||
@@ -99,7 +99,7 @@ it('toggles a filter chip with a click and with Space, and grows its check in',
|
||||
|
||||
// A key press first, so the browser is in keyboard modality; focused directly, not tabbed to,
|
||||
// because WebKit leaves form controls out of the Tab order unless full keyboard access is on.
|
||||
$page->keys('body', 'Tab');
|
||||
$page->keys('#content', 'Tab');
|
||||
$page->script(filterInput('archives').'.focus()');
|
||||
$page->keys(':focus', 'Space')
|
||||
->assertScript(filterInput('archives').'.checked')
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
const SNACKBAR = "document.querySelector('[x-data=\"materialSnackbar\"] [aria-live]')";
|
||||
|
||||
it('shows a toast as a snackbar, then the next in turn', function () {
|
||||
$page = visit('/material')->waitForEvent('networkidle')
|
||||
$page = visit('/material/communication')->waitForEvent('networkidle')
|
||||
->assertScript("document.readyState === 'complete' && typeof window.Alpine !== 'undefined' && typeof window.Livewire !== 'undefined'");
|
||||
|
||||
$page->script("materialToast('First', { type: 'success', timeout: 600 }); materialToast('Second', { type: 'error' })");
|
||||
@@ -18,7 +18,7 @@ it('shows a toast as a snackbar, then the next in turn', function () {
|
||||
});
|
||||
|
||||
it('shows a toast dispatched as a browser event, as the Toasts concern does', function () {
|
||||
$page = visit('/material')->waitForEvent('networkidle')
|
||||
$page = visit('/material/communication')->waitForEvent('networkidle')
|
||||
->assertScript("document.readyState === 'complete' && typeof window.Alpine !== 'undefined' && typeof window.Livewire !== 'undefined'");
|
||||
|
||||
// Through Livewire, in the page's own realm: an event built inside Playwright's evaluate
|
||||
@@ -29,7 +29,7 @@ it('shows a toast dispatched as a browser event, as the Toasts concern does', fu
|
||||
});
|
||||
|
||||
it('runs a toast\'s action and dismisses it', function () {
|
||||
$page = visit('/material')->waitForEvent('networkidle')
|
||||
$page = visit('/material/communication')->waitForEvent('networkidle')
|
||||
->assertScript("document.readyState === 'complete' && typeof window.Alpine !== 'undefined' && typeof window.Livewire !== 'undefined'");
|
||||
|
||||
$page->script("window.__undone = false; materialToast('Share deleted', { action: { label: 'Undo', handler: () => window.__undone = true } })");
|
||||
@@ -42,7 +42,7 @@ it('runs a toast\'s action and dismisses it', function () {
|
||||
it('opens a persistent rich tooltip on press', function () {
|
||||
$bubble = "document.querySelector('#communication [role=\"dialog\"][popover]')";
|
||||
|
||||
visit('/material')->waitForEvent('networkidle')
|
||||
visit('/material/communication')->waitForEvent('networkidle')
|
||||
->assertScript("document.readyState === 'complete' && typeof window.Alpine !== 'undefined' && typeof window.Livewire !== 'undefined'")
|
||||
->click('#communication button:has-text("Press for details")')
|
||||
->assertScript("{$bubble}.matches(':popover-open')");
|
||||
|
||||
@@ -66,7 +66,7 @@ function overlayProbe()
|
||||
|
||||
function containment()
|
||||
{
|
||||
return visit('/material')->waitForEvent('networkidle')
|
||||
return visit('/material/containment')->waitForEvent('networkidle')
|
||||
->assertScript("document.readyState === 'complete' && typeof window.Alpine !== 'undefined' && typeof window.Livewire !== 'undefined'");
|
||||
}
|
||||
|
||||
|
||||
@@ -33,10 +33,10 @@ class DateProbe extends Component
|
||||
<p>trip: <span id="trip">{{ json_encode($trip) }}</span></p>
|
||||
<p>renders: <span id="renders">{{ $renders }}</span></p>
|
||||
|
||||
<x-datepicker id="expires-field" label="Expires" wire:model.live="expires" min="2026-09-10" max="2026-10-20" />
|
||||
<x-datepicker id="expires-field" label="Expires" wire:model.live="expires" min="2026-09-10" max="2026-10-20" clearable />
|
||||
<x-datepicker id="birthday-field" label="Birthday" mode="modal" wire:model.live="birthday" />
|
||||
<x-datepicker id="delivery-field" label="Delivery" mode="input" wire:model.live="delivery" min="2026-01-01" />
|
||||
<x-datepicker id="trip-field" label="Trip" range wire:model.live="trip" />
|
||||
<x-datepicker id="trip-field" label="Trip" range wire:model.live="trip" clearable />
|
||||
</div>
|
||||
BLADE;
|
||||
}
|
||||
@@ -296,3 +296,18 @@ it('opens a docked picker as a modal one on a compact window', function () {
|
||||
->assertScript("document.querySelector('#expires-field-picker').matches(':modal')")
|
||||
->assertScript("getComputedStyle(document.querySelector('#expires-field-picker [data-datepicker-header]')).display !== 'none'");
|
||||
});
|
||||
|
||||
it('empties a date, or both ends of a range, with its clear button', function () {
|
||||
$page = dateProbe()
|
||||
->assertScript("getComputedStyle(document.querySelector('#expires-field').closest('.field').querySelector('[data-field-clear]')).display !== 'none'");
|
||||
|
||||
$page->click('[data-datepicker]:has(#expires-field) [data-field-clear]')
|
||||
->assertScript("document.querySelector('#expires').textContent === ''")
|
||||
->assertValue('#expires-field', '')
|
||||
->assertScript("document.activeElement.id === 'expires-field'")
|
||||
->assertScript("getComputedStyle(document.querySelector('[data-datepicker]:has(#expires-field) [data-field-clear]')).display === 'none'");
|
||||
|
||||
$page->click('[data-datepicker]:has(#trip-field) [data-field-clear]')
|
||||
->assertSeeIn('#trip', '{"start":null,"end":null}')
|
||||
->assertValue('#trip-field', '');
|
||||
});
|
||||
|
||||
@@ -15,7 +15,10 @@ function navigationReady(mixed $page): mixed
|
||||
|
||||
function shellPage(int $width = 1280, int $height = 900, string $path = '/material/shell'): mixed
|
||||
{
|
||||
return navigationReady(visit($path)->resize($width, $height));
|
||||
// The window is resized after the page starts loading, and an adaptive rail hears of the new
|
||||
// width from a media query's change event, which can arrive after the first key press.
|
||||
return navigationReady(visit($path)->resize($width, $height))
|
||||
->assertScript("window.eval(\"(() => { const rail = document.querySelector('[data-navigation-rail]'); return ! rail || rail.dataset.navigationRail !== 'adaptive' || Alpine.\$data(rail).wide === window.matchMedia('(min-width: 64rem)').matches; })()\")");
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -55,7 +55,7 @@ function progressShowcase(array $options = [])
|
||||
{
|
||||
// networkidle alone can return before a repeated visit has even loaded in Firefox; the
|
||||
// assertion retries until the page is complete and Alpine has started.
|
||||
return visit('/material', $options)->waitForEvent('networkidle')
|
||||
return visit('/material/progress', $options)->waitForEvent('networkidle')
|
||||
->assertScript("document.readyState === 'complete' && typeof window.Alpine !== 'undefined' && typeof window.Livewire !== 'undefined'");
|
||||
}
|
||||
|
||||
|
||||
@@ -5,3 +5,51 @@ it('opens the showcase in a real browser', function () {
|
||||
->assertSee('Livewire Material')
|
||||
->assertNoJavaScriptErrors();
|
||||
});
|
||||
|
||||
it('moves between sections through the rail and the next-section link, keeping the rail', function () {
|
||||
$page = visit('/material')->waitForEvent('networkidle')
|
||||
->assertScript("document.readyState === 'complete' && typeof window.Alpine !== 'undefined' && typeof window.Livewire !== 'undefined'");
|
||||
|
||||
$page->click('[data-navigation-rail-panel] a[href$="/material/buttons"]')
|
||||
->assertSee('Variants')
|
||||
->assertScript("document.querySelector('[data-navigation-rail-panel] [aria-current=\"page\"]').getAttribute('href').endsWith('/material/buttons')")
|
||||
->assertScript("document.title === 'Buttons · Livewire Material'");
|
||||
|
||||
$page->click('[data-test="next-section"]')
|
||||
->assertScript("location.pathname.endsWith('/material/menus')")
|
||||
->assertNoJavaScriptErrors();
|
||||
});
|
||||
|
||||
it('finds a component from anywhere and opens its section', function () {
|
||||
$page = visit('/material/buttons')->waitForEvent('networkidle')
|
||||
->assertScript("document.readyState === 'complete' && typeof window.Alpine !== 'undefined' && typeof window.Livewire !== 'undefined'");
|
||||
|
||||
$page->keys('#content', '/')
|
||||
->assertScript("document.activeElement.id === 'showcase-search'");
|
||||
|
||||
$page->type('#showcase-search', 'datepicker')
|
||||
->assertSeeIn('[data-search-view]', '<x-datepicker>');
|
||||
|
||||
$page->keys('#showcase-search', 'Enter')
|
||||
->assertScript("location.pathname.endsWith('/material/pickers')")
|
||||
->assertScript("document.title === 'Date pickers · Livewire Material'");
|
||||
});
|
||||
|
||||
it('opens an example on another page at the example', function () {
|
||||
$page = visit('/material')->waitForEvent('networkidle')
|
||||
->assertScript("document.readyState === 'complete' && typeof window.Alpine !== 'undefined' && typeof window.Livewire !== 'undefined'");
|
||||
|
||||
$page->click('#showcase-search')
|
||||
->type('#showcase-search', 'split button')
|
||||
->click('[data-showcase-result]:has-text("Example")')
|
||||
->assertScript("location.pathname.endsWith('/material/buttons') && location.hash.startsWith('#example-')")
|
||||
->assertScript('(() => { const box = document.getElementById(decodeURIComponent(location.hash.slice(1))).getBoundingClientRect(); return box.top >= 0 && box.top < innerHeight; })()');
|
||||
});
|
||||
|
||||
it('says so when nothing matches', function () {
|
||||
visit('/material')->waitForEvent('networkidle')
|
||||
->assertScript("document.readyState === 'complete' && typeof window.Alpine !== 'undefined' && typeof window.Livewire !== 'undefined'")
|
||||
->click('#showcase-search')
|
||||
->type('#showcase-search', 'zzzz')
|
||||
->assertSeeIn('[data-search-view]', 'Nothing matches');
|
||||
});
|
||||
|
||||
@@ -59,7 +59,7 @@ function sliderProbe()
|
||||
|
||||
function sliderShowcase()
|
||||
{
|
||||
return visit('/material')->waitForEvent('networkidle')
|
||||
return visit('/material/sliders')->waitForEvent('networkidle')
|
||||
->assertScript("document.readyState === 'complete' && typeof window.Alpine !== 'undefined' && typeof window.Livewire !== 'undefined'");
|
||||
}
|
||||
|
||||
|
||||
@@ -19,13 +19,15 @@ it('follows the operating system while the visitor has not chosen', function ()
|
||||
});
|
||||
|
||||
it('keeps the visitor\'s choice over the operating system', function () {
|
||||
// The theme switch waits for Alpine (x-cloak); a press before then would find nothing to press.
|
||||
$page = visit('/material')->inDarkMode()
|
||||
->waitForEvent('networkidle')
|
||||
->assertScript("document.readyState === 'complete' && typeof window.Alpine !== 'undefined' && typeof window.Livewire !== 'undefined'")
|
||||
->assertScript(theme('data-theme', 'dark'));
|
||||
|
||||
$page->click('@theme-light')
|
||||
$page->click('[data-theme-option="light"]')
|
||||
->assertScript(theme('data-theme', 'light'))
|
||||
->assertAttribute('@theme-light', 'aria-pressed', 'true')
|
||||
->assertAttribute('[data-theme-option="light"]', 'aria-checked', 'true')
|
||||
->assertScript("localStorage.getItem('material-theme') === 'light'");
|
||||
|
||||
$page->refresh()
|
||||
@@ -50,7 +52,7 @@ it('adopts an earlier toggle\'s choice once', function () {
|
||||
it('repaints a section that sets its own theme', function () {
|
||||
$swatch = fn (int $index): string => "getComputedStyle(document.querySelectorAll('#colour [data-theme] .bg-primary')[{$index}]).backgroundColor";
|
||||
|
||||
visit('/material')->inLightMode()
|
||||
visit('/material/colour')->inLightMode()
|
||||
->assertScript(theme('data-theme', 'light'))
|
||||
->assertScript("{$swatch(0)} !== {$swatch(1)}");
|
||||
});
|
||||
|
||||
@@ -163,3 +163,12 @@ it('replaces the hint with the errors for the property and its start and end', f
|
||||
->toContain('data-datepicker-error')
|
||||
->and(substr_count($html, 'data-invalid=""'))->toBe(2);
|
||||
});
|
||||
|
||||
it('offers a clear button on request', function () {
|
||||
expect((string) $this->blade('<x-datepicker label="Expires" clearable />'))
|
||||
->toContain('data-field-clear')
|
||||
->toContain('x-on:click="clear()"')
|
||||
->toContain('aria-label="Clear"')
|
||||
->and((string) $this->blade('<x-datepicker label="Expires" />'))
|
||||
->not->toContain('data-field-clear');
|
||||
});
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Blade;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Illuminate\Support\Facades\View;
|
||||
use NoNameWeb\LivewireMaterial\LivewireMaterialServiceProvider;
|
||||
|
||||
/**
|
||||
* The package's own component names, longest first.
|
||||
*
|
||||
* @return list<string>
|
||||
*/
|
||||
function packageComponentNames(): array
|
||||
{
|
||||
return collect(File::files(LivewireMaterialServiceProvider::componentPath()))
|
||||
->map(fn (SplFileInfo $file): string => $file->getBasename('.blade.php'))
|
||||
->sortByDesc(fn (string $name): int => strlen($name))
|
||||
->values()
|
||||
->all();
|
||||
}
|
||||
|
||||
it('refers to its own components through its namespace, which no prefix or app component changes', function () {
|
||||
$names = implode('|', array_map(fn (string $name): string => preg_quote($name, '/'), packageComponentNames()));
|
||||
|
||||
$unqualified = collect(File::allFiles(__DIR__.'/../../resources/views'))
|
||||
->filter(fn (SplFileInfo $file): bool => str_ends_with($file->getFilename(), '.blade.php'))
|
||||
->flatMap(function (SplFileInfo $file) use ($names): array {
|
||||
// Comments document usage as an application writes it, and the showcase's examples
|
||||
// are that usage; neither is compiled as the package's own markup.
|
||||
$source = preg_replace(['/\{\{--.*?--\}\}/s', "/<<<'BLADE'.*?^\\s*BLADE,/sm"], '', $file->getContents());
|
||||
|
||||
preg_match_all('/<\/?x-('.$names.')(?=[\s\/>])/', $source, $matches);
|
||||
|
||||
return array_map(fn (string $tag): string => $file->getRelativePathname().': '.$tag, $matches[0]);
|
||||
})
|
||||
->values();
|
||||
|
||||
expect($unqualified->all())->toBe([]);
|
||||
});
|
||||
|
||||
it('renders components that use others under a prefix', function () {
|
||||
config(['livewire-material.prefix' => 'm']);
|
||||
|
||||
Blade::anonymousComponentPath(LivewireMaterialServiceProvider::componentPath(), 'm');
|
||||
|
||||
expect(Blade::render('<x-m::drawer title="Share" with-close-button separator><x-m::input label="Name" clearable /></x-m::drawer>'))
|
||||
->toContain('aria-label="Close"')
|
||||
->toContain('role="separator"')
|
||||
->toContain('data-field-clear');
|
||||
});
|
||||
|
||||
it('is not shadowed by an application component of the same name', function () {
|
||||
$views = sys_get_temp_dir().'/livewire-material-shadow-'.uniqid();
|
||||
File::ensureDirectoryExists($views.'/components');
|
||||
File::put($views.'/components/button.blade.php', '<span>the application\'s button</span>');
|
||||
View::getFinder()->prependLocation($views);
|
||||
|
||||
try {
|
||||
expect(Blade::render('<x-button />'))->toContain('the application\'s button')
|
||||
->and(Blade::render('<x-drawer title="Share" with-close-button />'))->not->toContain('the application\'s button')
|
||||
->toContain('aria-label="Close"');
|
||||
} finally {
|
||||
File::deleteDirectory($views);
|
||||
}
|
||||
});
|
||||
|
||||
it('shows the showcase\'s examples as an application with a prefix writes them', function () {
|
||||
config(['livewire-material.prefix' => 'm']);
|
||||
Blade::anonymousComponentPath(LivewireMaterialServiceProvider::componentPath(), 'm');
|
||||
|
||||
expect(Blade::render('<x-showcase::example title="Buttons" :code="$code" />', ['code' => '<x-button label="Save" />']))
|
||||
->toContain('<x-m::button label="Save" />')
|
||||
->toContain('Save</span>');
|
||||
});
|
||||
@@ -26,12 +26,12 @@ it('registers the components without a prefix', function () {
|
||||
});
|
||||
|
||||
it('registers the components under a configured prefix', function () {
|
||||
config(['livewire-material.prefix' => 'm-']);
|
||||
config(['livewire-material.prefix' => 'm']);
|
||||
|
||||
$provider = app()->getProvider(LivewireMaterialServiceProvider::class);
|
||||
(fn () => $this->registerComponents())->call($provider);
|
||||
|
||||
expect(packageComponentPaths()->last()['prefix'])->toBe('m-');
|
||||
expect(packageComponentPaths()->last()['prefix'])->toBe('m');
|
||||
});
|
||||
|
||||
it('keeps blade-icons from registering an <x-icon> that would shadow ours', function () {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\File;
|
||||
use NoNameWeb\LivewireMaterial\Showcase\Sections;
|
||||
use NoNameWeb\LivewireMaterial\Support\SvgFile;
|
||||
|
||||
/**
|
||||
@@ -27,6 +29,42 @@ it('mounts the showcase when enabled', function () {
|
||||
->assertSee('Livewire Material');
|
||||
});
|
||||
|
||||
it('gives every section a page of its own, linked from the overview and the rail', function () {
|
||||
$overview = $this->withoutVite()->get('/material')->assertOk();
|
||||
|
||||
foreach (Sections::all() as $key => $section) {
|
||||
$overview->assertSee(route('livewire-material.section', $key), false);
|
||||
|
||||
$this->withoutVite()
|
||||
->get("/material/{$key}")
|
||||
->assertOk()
|
||||
->assertSee("<title>{$section['title']} · Livewire Material</title>", false)
|
||||
->assertSee('id="'.$key.'"', false);
|
||||
}
|
||||
|
||||
$this->get('/material/not-a-section')->assertNotFound();
|
||||
});
|
||||
|
||||
it('indexes every section, example and component for the search, each linking to a place that exists', function () {
|
||||
$index = collect($this->getJson('/material/search.json')->assertOk()->json());
|
||||
|
||||
expect($index->where('kind', 'Section')->pluck('title')->all())
|
||||
->toBe(collect(Sections::all())->pluck('title')->all())
|
||||
->and($index->where('kind', 'Component')->pluck('title')->all())
|
||||
->toContain('<x-datepicker>', '<x-button>', '<x-app-shell>')
|
||||
->and($index->firstWhere('title', '<x-datepicker>')['url'])->toBe('/material/pickers');
|
||||
|
||||
$index->where('kind', 'Example')
|
||||
->groupBy(fn (array $entry): string => strtok($entry['url'], '#'))
|
||||
->each(function ($examples, string $page): void {
|
||||
$html = $this->withoutVite()->get($page)->assertOk()->getContent();
|
||||
|
||||
foreach ($examples as $example) {
|
||||
expect($html)->toContain('id="'.substr($example['url'], strpos($example['url'], '#') + 1).'"');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('does not mount the showcase when disabled', function () {
|
||||
rebootWithShowcase(false);
|
||||
|
||||
@@ -68,3 +106,17 @@ it('previews a sample mail in the package theme', function () {
|
||||
->assertSee('border-radius: 9999px', false)
|
||||
->assertSee('Unsubscribe');
|
||||
});
|
||||
|
||||
it('shows every component somewhere in the showcase', function () {
|
||||
$showcase = collect(File::allFiles(__DIR__.'/../../resources/views/showcase'))
|
||||
->map(fn (SplFileInfo $file): string => $file->getContents())
|
||||
->implode("\n");
|
||||
|
||||
$absent = collect(File::files(__DIR__.'/../../resources/views/components'))
|
||||
->map(fn (SplFileInfo $file): string => $file->getBasename('.blade.php'))
|
||||
->reject(fn (string $name): bool => preg_match('/(?:<|<)x-(?:livewire-material::)?'.preg_quote($name, '/').'(?:[\s\/>]|>)/', $showcase) === 1)
|
||||
->values()
|
||||
->all();
|
||||
|
||||
expect($absent)->toBe([]);
|
||||
});
|
||||
|
||||
@@ -3,3 +3,9 @@
|
||||
use NoNameWeb\LivewireMaterial\Tests\TestCase;
|
||||
|
||||
pest()->extend(TestCase::class)->in('Feature', 'Browser');
|
||||
|
||||
// A shared CI runner is slower than a workstation: an animation or a smooth scroll can take longer
|
||||
// than the default five seconds to settle there. BROWSER_TIMEOUT (milliseconds) raises the limit.
|
||||
if (($timeout = (int) getenv('BROWSER_TIMEOUT')) > 0) {
|
||||
pest()->browser()->timeout($timeout);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user