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
[$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', []));
// load the user's calendars
@ -106,10 +111,15 @@ class CalendarController extends Controller
$payload = [
'view' => $view,
'range' => $range,
'active' => [
'nav' => [
'prev' => $prev,
'next' => $next,
'today' => $today,
],
'active' => [
'year' => $range['start']->format('Y'),
'month' => $range['start']->format("F"),
'day' => $range['start']->format("d"),
'day' => $range['start']->format("d"),
],
'calendars' => $calendar_map->map(function ($cal) {
return [

View File

@ -22,118 +22,133 @@ body {
}
}
}
}
/* primary app navigation on the left */
nav {
@apply w-20 flex flex-col items-center justify-between;
/* primary app navigation on the left */
> nav {
@apply w-20 flex flex-col items-center justify-between;
/* top items */
.top {
@apply flex flex-col items-center pt-6 2xl:pt-8 mt-2px;
}
/* top items */
.top {
@apply flex flex-col items-center pt-6 2xl:pt-8 mt-2px;
}
/* bottom items */
/* bottom items */
.bottom {
@apply pb-6 2xl:pb-8;
}
.bottom {
@apply pb-6 2xl:pb-8;
}
/* app buttons */
menu {
@apply flex flex-col gap-1 items-center mt-6;
/* app buttons */
menu {
@apply flex flex-col gap-1 items-center mt-6;
li.app-button {
li.app-button {
a {
@apply flex items-center justify-center p-3 bg-transparent text-black;
transition: background-color 100ms ease-in-out;
border-radius: 70% 50% 70% 30% / 60% 60% 60% 40%; /* blob 1 */
&:hover {
@apply bg-gray-200;
}
&.is-active {
@apply bg-cyan-400;
a {
@apply flex items-center justify-center p-3 bg-transparent text-black;
transition: background-color 100ms ease-in-out;
border-radius: 70% 50% 70% 30% / 60% 60% 60% 40%; /* blob 1 */
&:hover {
@apply bg-cyan-500;
@apply bg-gray-200;
}
&.is-active {
@apply bg-cyan-400;
&:hover {
@apply bg-cyan-500;
}
}
}
}
&:nth-child(2) a {
border-radius: 70% 30% 30% 70% / 60% 40% 60% 40%; /* blob 2 */
}
&:nth-child(2) a {
border-radius: 70% 30% 30% 70% / 60% 40% 60% 40%; /* blob 2 */
}
&:nth-child(3) a {
border-radius: 80% 65% 90% 50% / 90% 80% 75% 75%; /* blob 3 */
&:nth-child(3) a {
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 {
@apply rounded-lg bg-white;
/* app */
body#app & {
@apply grid m-2 ml-0;
grid-template-rows: 5rem auto;
@apply grid grid-cols-1 m-2 ml-0;
/* if there's an aside, set the cols */
&:has(aside) {
grid-template-columns: minmax(20rem, 20dvw) auto;
}
}
/* auth screens */
body#auth & {
@apply w-1/2 mx-auto p-8;
min-width: 16rem;
max-width: 40rem;
}
/* main content title and actions */
> header {
@apply grid items-center;
grid-template-columns: minmax(20rem, 20dvw) repeat(3, 1fr);
/* left column */
aside {
@apply flex flex-col col-span-1 px-6 2xl:px-8 pb-8 h-full;
h1 {
@apply flex items-center pl-6 2xl:pl-8;
}
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;
> h1 {
@apply flex items-center h-20;
}
}
/* main content wrapper */
> article {
@apply grid w-full;
grid-template-columns: minmax(20rem, 20dvw) repeat(3, 1fr);
article {
@apply grid grid-cols-1 w-full;
grid-template-rows: 5rem auto;
/* left column */
aside {
@apply col-span-1 px-6 2xl:px-8 h-full;
}
/* main content title and actions */
> header {
@apply flex flex-row items-center justify-between w-full;
/* calendar page defaults */
&#calendar {
/* if h1 exists it means there's no aside, so force the width from that */
h1 {
@apply flex items-center pl-6 2xl:pl-8;
width: minmax(20rem, 20dvw);
}
aside {
@apply grid pb-6 2xl:pb-8;
grid-template-rows: 1fr min-content;
h2 {
@apply flex flex-row gap-1 items-center justify-start relative top-px;
> span {
@apply text-gray-700;
}
}
menu {
@apply flex flex-row items-center justify-end gap-2;
}
}
}
}
@media (width >= 96rem) { /* 2xl */
main {
body#app & {
aside {
> h1 {
@apply h-22;
}
}
article {
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 {
@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 border-md border-primary font-medium;
box-shadow: 1.5px 2.5px 0 0 var(--color-primary);
@apply border-md border-primary border-l-0 font-medium rounded-none;
transition: background-color 100ms ease-in-out;
> input[type="radio"] {
@apply hidden absolute top-0 left-0 w-0 h-0 max-w-0 max-h-0;
&:hover {
@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 {
@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 {
@apply border-r-md rounded-r-md;
}
}
&:has(input:checked) {
@apply bg-cyan-300 border-t-2;
box-shadow: inset 0 0.25rem 0 0 var(--color-cyan-400);
left: 1.5px;
top: 2.5px;
> label {
> input[type="radio"] {
@apply hidden absolute top-0 left-0 w-0 h-0 max-w-0 max-h-0;
}
}
&: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-slot name="header">
<x-slot name="aside">
<h1>
{{ __('Calendar') }}
</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>
<strong>{{ $active['month'] }}</strong>
<span>{{ $active['year'] }}</span>
</h2>
<menu>
<li>
<form class="button-group button-group--primary" method="get" action="/">
<x-button.group-button>Day</x-button.group-button>
<x-button.group-button>Week</x-button.group-button>
<x-button.group-button active="true">Month</x-button.group-button>
<x-button.group-button>3-Up</x-button.group-button>
<form id="calendar-nav"
action="{{ route('calendar.index') }}"
method="get"
hx-get="{{ route('calendar.index') }}"
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>
<li>
<a class="button button--primary" href="{{ route('calendar.create') }}">
@ -30,48 +99,7 @@
</x-slot>
<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-slot>
</x-app-layout>

View File

@ -1,11 +1,10 @@
@props([
'type' => 'submit',
'name' => 'button-group',
'value' => '1',
'value' => '',
'class' => '',
'active' => false ])
])
<label class="{{ $class }}">
<input type="radio" name="{{ $name }}" value="{{ $value }}" @checked($active)>
<button type="{{ $type }}" name="{{ $name }}" value="{{ $value }}" class="{{ $class }}">
{{ $slot }}
</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 -->
<main>
@isset($header)
<header>
{{ $header }}
</header>
@isset($aside)
<aside>
{{ $aside }}
</aside>
@endisset
<article {{ $attributes }}>
@isset($header)
<header>
{{ $header }}
</header>
@endisset
{{ $article ?? $slot }}
</article>
</main>
<!-- messages -->
<aside>
<figure>
@if (session('toast'))
<div
<figcaption
x-data="{ open: true }"
x-show="open"
x-init="setTimeout(() => open = false, 4000)"
@ -36,9 +46,9 @@
x-transition.opacity.duration.300ms
>
{{ session('toast') }}
</div>
</figcaption>
@endif
</aside>
</figure>
<!-- modal -->
<x-modal />