Solves CSS riddle with perfect group buttons, changes layout to better handle content pane, adds calendar date navigation

This commit is contained in:
Andrew Gioia 2025-07-30 14:22:56 -04:00
parent 9f3ceabd7d
commit a4adab31d2
Signed by: andrew
GPG Key ID: FC09694A000800C8
7 changed files with 242 additions and 143 deletions

View File

@ -36,7 +36,12 @@ class CalendarController extends Controller
// get the view and time range // get the view and time range
[$view, $range] = $this->resolveRange($request); [$view, $range] = $this->resolveRange($request);
// get the user's selected calendars // date range controls
$prev = $range['start']->copy()->subMonth()->startOfMonth()->toDateString();
$next = $range['start']->copy()->addMonth()->startOfMonth()->toDateString();
$today = Carbon::today()->toDateString();
// get the user's visible calendars from the left bar
$visible = collect($request->query('c', [])); $visible = collect($request->query('c', []));
// load the user's calendars // load the user's calendars
@ -106,10 +111,15 @@ class CalendarController extends Controller
$payload = [ $payload = [
'view' => $view, 'view' => $view,
'range' => $range, 'range' => $range,
'active' => [ 'nav' => [
'prev' => $prev,
'next' => $next,
'today' => $today,
],
'active' => [
'year' => $range['start']->format('Y'), 'year' => $range['start']->format('Y'),
'month' => $range['start']->format("F"), 'month' => $range['start']->format("F"),
'day' => $range['start']->format("d"), 'day' => $range['start']->format("d"),
], ],
'calendars' => $calendar_map->map(function ($cal) { 'calendars' => $calendar_map->map(function ($cal) {
return [ return [

View File

@ -22,118 +22,133 @@ body {
} }
} }
} }
}
/* primary app navigation on the left */ /* primary app navigation on the left */
nav { > nav {
@apply w-20 flex flex-col items-center justify-between; @apply w-20 flex flex-col items-center justify-between;
/* top items */ /* top items */
.top { .top {
@apply flex flex-col items-center pt-6 2xl:pt-8 mt-2px; @apply flex flex-col items-center pt-6 2xl:pt-8 mt-2px;
} }
/* bottom items */ /* bottom items */
.bottom { .bottom {
@apply pb-6 2xl:pb-8; @apply pb-6 2xl:pb-8;
} }
/* app buttons */ /* app buttons */
menu { menu {
@apply flex flex-col gap-1 items-center mt-6; @apply flex flex-col gap-1 items-center mt-6;
li.app-button { li.app-button {
a { a {
@apply flex items-center justify-center p-3 bg-transparent text-black; @apply flex items-center justify-center p-3 bg-transparent text-black;
transition: background-color 100ms ease-in-out; transition: background-color 100ms ease-in-out;
border-radius: 70% 50% 70% 30% / 60% 60% 60% 40%; /* blob 1 */ border-radius: 70% 50% 70% 30% / 60% 60% 60% 40%; /* blob 1 */
&:hover {
@apply bg-gray-200;
}
&.is-active {
@apply bg-cyan-400;
&:hover { &:hover {
@apply bg-cyan-500; @apply bg-gray-200;
}
&.is-active {
@apply bg-cyan-400;
&:hover {
@apply bg-cyan-500;
}
} }
} }
}
&:nth-child(2) a { &:nth-child(2) a {
border-radius: 70% 30% 30% 70% / 60% 40% 60% 40%; /* blob 2 */ border-radius: 70% 30% 30% 70% / 60% 40% 60% 40%; /* blob 2 */
} }
&:nth-child(3) a { &:nth-child(3) a {
border-radius: 80% 65% 90% 50% / 90% 80% 75% 75%; /* blob 3 */ border-radius: 80% 65% 90% 50% / 90% 80% 75% 75%; /* blob 3 */
}
} }
} }
} }
} }
/* primary content window defaults */ /*
* primary content window defaults
*
* main
* aside (optional left bar, must include h1 page title)
* article (content pane; if no aside, header includes h1)
* header
* section
*/
main { main {
@apply rounded-lg bg-white; @apply rounded-lg bg-white;
/* app */
body#app & { body#app & {
@apply grid m-2 ml-0; @apply grid grid-cols-1 m-2 ml-0;
grid-template-rows: 5rem auto;
/* if there's an aside, set the cols */
&:has(aside) {
grid-template-columns: minmax(20rem, 20dvw) auto;
}
} }
/* auth screens */
body#auth & { body#auth & {
@apply w-1/2 mx-auto p-8; @apply w-1/2 mx-auto p-8;
min-width: 16rem; min-width: 16rem;
max-width: 40rem; max-width: 40rem;
} }
/* main content title and actions */ /* left column */
> header { aside {
@apply grid items-center; @apply flex flex-col col-span-1 px-6 2xl:px-8 pb-8 h-full;
grid-template-columns: minmax(20rem, 20dvw) repeat(3, 1fr);
h1 { > h1 {
@apply flex items-center pl-6 2xl:pl-8; @apply flex items-center h-20;
}
h2 {
@apply col-span-1 flex flex-row gap-1 items-center justify-start relative top-px;
> span {
@apply text-gray-700;
}
}
menu {
@apply col-span-2 flex flex-row items-center justify-end gap-2 pr-6 2xl:pr-8;
} }
} }
/* main content wrapper */ /* main content wrapper */
> article { article {
@apply grid w-full; @apply grid grid-cols-1 w-full;
grid-template-columns: minmax(20rem, 20dvw) repeat(3, 1fr); grid-template-rows: 5rem auto;
/* left column */ /* main content title and actions */
aside { > header {
@apply col-span-1 px-6 2xl:px-8 h-full; @apply flex flex-row items-center justify-between w-full;
}
/* calendar page defaults */ /* if h1 exists it means there's no aside, so force the width from that */
&#calendar { h1 {
@apply flex items-center pl-6 2xl:pl-8;
width: minmax(20rem, 20dvw);
}
aside { h2 {
@apply grid pb-6 2xl:pb-8; @apply flex flex-row gap-1 items-center justify-start relative top-px;
grid-template-rows: 1fr min-content;
> span {
@apply text-gray-700;
}
}
menu {
@apply flex flex-row items-center justify-end gap-2;
} }
} }
} }
} }
@media (width >= 96rem) { /* 2xl */ @media (width >= 96rem) { /* 2xl */
main { main {
body#app & { aside {
> h1 {
@apply h-22;
}
}
article {
grid-template-rows: 5.5rem auto; grid-template-rows: 5.5rem auto;
} }
} }

View File

@ -32,31 +32,58 @@ button,
} }
} }
/* button groups are used with labels/checks as well as regular buttons */
.button-group { .button-group {
@apply relative flex flex-row items-center p-0 m-0 h-11 max-h-11; @apply relative flex flex-row items-center p-0 m-0 h-11 max-h-11 rounded-md;
box-shadow: 2.5px 2.5px 0 0 var(--color-primary);
> label { > label,
> button {
@apply relative flex items-center justify-center h-full pl-3.5 pr-3 cursor-pointer; @apply relative flex items-center justify-center h-full pl-3.5 pr-3 cursor-pointer;
@apply border-md border-primary font-medium; @apply border-md border-primary border-l-0 font-medium rounded-none;
box-shadow: 1.5px 2.5px 0 0 var(--color-primary); transition: background-color 100ms ease-in-out;
> input[type="radio"] { &:hover {
@apply hidden absolute top-0 left-0 w-0 h-0 max-w-0 max-h-0; @apply bg-cyan-300;
}
&:has(input:checked),
&:active {
@apply bg-cyan-300;
box-shadow:
inset 2.5px 0 0 0 var(--color-primary),
inset 0 0.25rem 0 0 var(--color-cyan-400);
left: 0;
top: 2.5px;
+ label {
box-shadow:
inset 1.5px 0 0 0 var(--color-primary);
}
} }
&:first-child { &:first-child {
@apply rounded-l-md; @apply rounded-l-md border-l-md;
&:has(input:checked),
&:active {
box-shadow: inset 0 0.25rem 0 0 var(--color-cyan-400);
}
} }
&:last-child { &:last-child {
@apply border-r-md rounded-r-md; @apply border-r-md rounded-r-md;
} }
}
&:has(input:checked) { > label {
@apply bg-cyan-300 border-t-2; > input[type="radio"] {
box-shadow: inset 0 0.25rem 0 0 var(--color-cyan-400); @apply hidden absolute top-0 left-0 w-0 h-0 max-w-0 max-h-0;
left: 1.5px;
top: 2.5px;
} }
} }
&:has(> :last-child input:checked),
&:has(> :last-child:active) {
box-shadow: 1.5px 4.5px 0 -2px var(--color-primary);
}
} }

View File

@ -1,20 +1,89 @@
<x-app-layout id="calendar"> <x-app-layout id="calendar">
<x-slot name="header"> <x-slot name="aside">
<h1> <h1>
{{ __('Calendar') }} {{ __('Calendar') }}
</h1> </h1>
<div class="grow flex flex-col gap-4">
<details open>
<summary>{{ __('My Calendars') }}</summary>
<form id="calendar-toggles"
class="content"
action="{{ route('calendar.index') }}"
method="get">
<ul>
@foreach ($calendars as $cal)
<li>
<label class="flex items-center space-x-2">
<input type="checkbox"
class="calendar-toggle"
name="c[]"
value="{{ $cal['slug'] }}"
style="--checkbox-color: {{ $cal['color'] }}"
@checked($cal['visible'])>
<span>{{ $cal['name'] }}</span>
</label>
</li>
@endforeach
</ul>
{{-- fallback submit button for no-JS environments --}}
<noscript>
<button type="submit">{{ __('Apply') }}</button>
</noscript>
</form>
</details>
</div>
<x-calendar.mini>
@foreach ($grid['weeks'] as $week)
@foreach ($week as $day)
<x-calendar.mini-day :day="$day" />
@endforeach
@endforeach
</x-calendar.mini>
</x-slot>
<x-slot name="header">
<h2> <h2>
<strong>{{ $active['month'] }}</strong> <strong>{{ $active['month'] }}</strong>
<span>{{ $active['year'] }}</span> <span>{{ $active['year'] }}</span>
</h2> </h2>
<menu> <menu>
<li> <li>
<form class="button-group button-group--primary" method="get" action="/"> <form id="calendar-nav"
<x-button.group-button>Day</x-button.group-button> action="{{ route('calendar.index') }}"
<x-button.group-button>Week</x-button.group-button> method="get"
<x-button.group-button active="true">Month</x-button.group-button> hx-get="{{ route('calendar.index') }}"
<x-button.group-button>3-Up</x-button.group-button> hx-target="#calendar"
hx-select="#calendar"
hx-swap="outerHTML"
hx-push-url="true"
hx-include="#calendar-toggles">
{{-- keep current view (month/week/4day) --}}
<input type="hidden" name="view" value="{{ $view }}">
<nav class="button-group button-group--primary">
<x-button.group-button type="submit" name="date" value="{{ $nav['prev'] }}">
<x-icon-chevron-left />
</x-button.group-button>
<x-button.group-button type="submit" name="date" value="{{ $nav['today'] }}">
Today
</x-button.group-button>
<x-button.group-button type="submit" name="date" value="{{ $nav['next'] }}">
<x-icon-chevron-right />
</x-button.group-button>
</nav>
<noscript>
{{-- not needed, buttons already submit the form --}}
</noscript>
</form>
</li>
<li>
<form id="calendar-view" class="button-group button-group--primary" method="get" action="/">
<x-button.group-input>Day</x-button.group-button>
<x-button.group-input>Week</x-button.group-button>
<x-button.group-input active="true">Month</x-button.group-button>
<x-button.group-input>3-Up</x-button.group-button>
</form> </form>
<li> <li>
<a class="button button--primary" href="{{ route('calendar.create') }}"> <a class="button button--primary" href="{{ route('calendar.create') }}">
@ -30,48 +99,7 @@
</x-slot> </x-slot>
<x-slot name="article"> <x-slot name="article">
<aside>
<div class="flex flex-col gap-4">
<details open>
<summary>{{ __('My Calendars') }}</summary>
<form id="calendar-toggles"
class="content"
action="{{ route('calendar.index') }}"
method="get">
<ul>
@foreach ($calendars as $cal)
<li>
<label class="flex items-center space-x-2">
<input type="checkbox"
class="calendar-toggle"
name="c[]"
value="{{ $cal['slug'] }}"
style="--checkbox-color: {{ $cal['color'] }}"
@checked($cal['visible'])>
<span>{{ $cal['name'] }}</span>
</label>
</li>
@endforeach
</ul>
{{-- fallback submit button for no-JS environments --}}
<noscript>
<button type="submit">{{ __('Apply') }}</button>
</noscript>
</form>
</details>
</div>
<x-calendar.mini>
@foreach ($grid['weeks'] as $week)
@foreach ($week as $day)
<x-calendar.mini-day :day="$day" />
@endforeach
@endforeach
</x-calendar.mini>
</aside>
<x-calendar.full class="month" :grid="$grid" :calendars="$calendars" :events="$events" /> <x-calendar.full class="month" :grid="$grid" :calendars="$calendars" :events="$events" />
</x-slot> </x-slot>
</x-app-layout> </x-app-layout>

View File

@ -1,11 +1,10 @@
@props([ @props([
'type' => 'submit', 'type' => 'submit',
'name' => 'button-group', 'name' => 'button-group',
'value' => '1', 'value' => '',
'class' => '', 'class' => '',
'active' => false ]) ])
<label class="{{ $class }}"> <button type="{{ $type }}" name="{{ $name }}" value="{{ $value }}" class="{{ $class }}">
<input type="radio" name="{{ $name }}" value="{{ $value }}" @checked($active)>
{{ $slot }} {{ $slot }}
</label> </label>

View File

@ -0,0 +1,10 @@
@props([
'name' => 'button-group',
'value' => '1',
'class' => '',
'active' => false ])
<label class="{{ $class }}">
<input type="radio" name="{{ $name }}" value="{{ $value }}" @checked($active)>
{{ $slot }}
</label>

View File

@ -14,21 +14,31 @@
<!-- content --> <!-- content -->
<main> <main>
@isset($header)
<header> @isset($aside)
{{ $header }} <aside>
</header> {{ $aside }}
</aside>
@endisset @endisset
<article {{ $attributes }}> <article {{ $attributes }}>
@isset($header)
<header>
{{ $header }}
</header>
@endisset
{{ $article ?? $slot }} {{ $article ?? $slot }}
</article> </article>
</main> </main>
<!-- messages --> <!-- messages -->
<aside> <figure>
@if (session('toast')) @if (session('toast'))
<div <figcaption
x-data="{ open: true }" x-data="{ open: true }"
x-show="open" x-show="open"
x-init="setTimeout(() => open = false, 4000)" x-init="setTimeout(() => open = false, 4000)"
@ -36,9 +46,9 @@
x-transition.opacity.duration.300ms x-transition.opacity.duration.300ms
> >
{{ session('toast') }} {{ session('toast') }}
</div> </figcaption>
@endif @endif
</aside> </figure>
<!-- modal --> <!-- modal -->
<x-modal /> <x-modal />