diff --git a/app/Http/Controllers/CalendarController.php b/app/Http/Controllers/CalendarController.php index c11d4ea..52c23c1 100644 --- a/app/Http/Controllers/CalendarController.php +++ b/app/Http/Controllers/CalendarController.php @@ -41,7 +41,8 @@ class CalendarController extends Controller 'calendars.id', 'ci.displayname', 'ci.calendarcolor', - 'meta.color as meta_color' + 'meta.color as meta_color', + 'meta.color_fg as meta_color_fg' ) ->join('calendarinstances as ci', 'ci.calendarid', '=', 'calendars.id') ->leftJoin('calendar_meta as meta', 'meta.calendar_id', '=', 'calendars.id') @@ -63,12 +64,18 @@ class CalendarController extends Controller $payload = [ 'view' => $view, 'range' => $range, + 'active' => [ + 'year' => $range['start']->format('Y'), + 'month' => $range['start']->format("F"), + 'day' => $range['start']->format("d"), + ], 'calendars' => $calendars->keyBy('id')->map(function ($cal) { return [ 'id' => $cal->id, 'name' => $cal->displayname, - 'color' => $cal->meta_color ?? $cal->calendarcolor ?? '#999', - 'on' => true, // default to visible; the UI can toggle this + 'color' => $cal->meta_color ?? $cal->calendarcolor ?? '#1a1a1a', // clean this up @todo + 'color_fg' => $cal->meta_color_fg ?? '#ffffff', // clean this up + 'on' => true, // default to visible; the UI can toggle this ]; }), 'events' => $events->map(function ($e) { // just the events map @@ -83,7 +90,9 @@ class CalendarController extends Controller 'calendar_id' => $e->calendarid, 'title' => $e->meta->title ?? '(no title)', 'start' => $start->format('c'), + 'start_ui' => $start->format('g:ia'), 'end' => optional($end)->format('c'), + 'end_ui' => optional($end)->format('g:ia') ]; }), 'grid' => $grid, @@ -129,7 +138,8 @@ class CalendarController extends Controller // update calendar meta $instance->meta()->create([ 'calendar_id' => $instanceId, - 'color' => $data['color'] ?? null, + 'color' => $data['color'] ?? '#1a1a1a', + 'color_fg' => contrast_text_color($data['color'] ?? '#1a1a1a'), 'created_at' => now(), 'updated_at' => now(), ]); @@ -212,8 +222,9 @@ class CalendarController extends Controller // update calendar meta (our table) $calendar->meta()->updateOrCreate([], [ - 'color' => $data['color'] ?? null] - ); + 'color' => $data['color'] ?? '#1a1a1a', + 'color_fg' => contrast_text_color($data['color'] ?? '#1a1a1a') + ]); return redirect() ->route('calendar.show', $calendar) @@ -313,7 +324,9 @@ class CalendarController extends Controller 'calendar_id' => $ev->calendarid, 'title' => $ev->meta->title ?? '(no title)', 'start' => $start->format('c'), - 'end' => $end->format('c'), + 'start_ui' => $start->format('g:ia'), + 'end' => optional($end)->format('c'), + 'end_ui' => optional($end)->format('g:ia') ]; } } diff --git a/app/Models/CalendarMeta.php b/app/Models/CalendarMeta.php index 20eadb8..0487ae7 100644 --- a/app/Models/CalendarMeta.php +++ b/app/Models/CalendarMeta.php @@ -15,6 +15,7 @@ class CalendarMeta extends Model protected $fillable = [ 'calendar_id', 'color', + 'color_fg', 'created_at', 'edited_at', ]; diff --git a/app/Support/helpers.php b/app/Support/helpers.php new file mode 100644 index 0000000..ad03fb7 --- /dev/null +++ b/app/Support/helpers.php @@ -0,0 +1,67 @@ + 4, use white. + * + * @param string $hex Background colour (3- or 6-digit hex, with or without #) + * @param string $light Return value for “white” (default '#ffffff') + * @param string $dark Return value for “black” (default '#000000') + * @return string + */ + function contrast_text_color(string $hex, string $light = '#ffffff', string $dark = '#000000'): string + { + // --- normalise ---------------------------------------------------- + $hex = ltrim($hex, '#'); + if (strlen($hex) === 3) { // #abc → #aabbcc + $hex = preg_replace('/./', '$0$0', $hex); + } + + [$r, $g, $b] = [ + hexdec(substr($hex, 0, 2)) / 255, + hexdec(substr($hex, 2, 2)) / 255, + hexdec(substr($hex, 4, 2)) / 255, + ]; + + // --- convert sRGB → linear RGB ----------------------------------- + $linear = function (float $c): float { + return $c <= 0.04045 ? $c / 12.92 : pow(($c + 0.055) / 1.055, 2.4); + }; + + $R = $linear($r); + $G = $linear($g); + $B = $linear($b); + + // --- relative luminance (ITU-R BT.709) ---------------------------- + $L_bg = 0.2126 * $R + 0.7152 * $G + 0.0722 * $B; // 0–1 + + // --- contrast ratios vs black (L=0) and white (L=1) --------------- + $contrast_black = ($L_bg + 0.05) / 0.05; // bg lighter than black + $contrast_white = 1.05 / ($L_bg + 0.05); // white vs bg + + // --- pick the winner --------------------------------------------- + $useDark = $contrast_black >= $contrast_white; + + // override rule if dark is true but white "looks better" + if ($useDark && $contrast_black < 5.5 && $contrast_white > 4) { + $useDark = false; // switch to white + } + + return $useDark ? $dark : $light; + } +} diff --git a/composer.json b/composer.json index 2747689..6f95567 100644 --- a/composer.json +++ b/composer.json @@ -28,7 +28,10 @@ "App\\": "app/", "Database\\Factories\\": "database/factories/", "Database\\Seeders\\": "database/seeders/" - } + }, + "files": [ + "app/Support/helpers.php" + ] }, "autoload-dev": { "psr-4": { diff --git a/database/migrations/2025_07_15_000001_create_calendar_meta_table.php b/database/migrations/2025_07_15_000001_create_calendar_meta_table.php index 4856e58..1fb4aaa 100644 --- a/database/migrations/2025_07_15_000001_create_calendar_meta_table.php +++ b/database/migrations/2025_07_15_000001_create_calendar_meta_table.php @@ -9,9 +9,10 @@ return new class extends Migration public function up(): void { Schema::create('calendar_meta', function (Blueprint $table) { - $table->unsignedInteger('calendar_id')->primary(); // FK = PK - $table->string('title')->nullable(); // UI override - $table->string('color', 7)->nullable(); // e.g. #FFAA00 + $table->unsignedInteger('calendar_id')->primary(); // FK = PK + $table->string('title')->nullable(); // ui override + $table->string('color', 7)->nullable(); // bg color + $table->string('color_fg', 7)->nullable(); // fg color $table->boolean('is_shared')->default(false); $table->json('settings')->nullable(); // arbitrary JSON $table->timestamps(); diff --git a/resources/css/app.css b/resources/css/app.css index 7a5a9c2..066113d 100644 --- a/resources/css/app.css +++ b/resources/css/app.css @@ -5,7 +5,12 @@ /** kithkin */ @import './etc/layout.css'; @import './etc/type.css'; +@import './lib/accordion.css'; @import './lib/button.css'; +@import './lib/calendar.css'; +@import './lib/checkbox.css'; +@import './lib/indicator.css'; +@import './lib/input.css'; @import './lib/mini.css'; /** plugins */ diff --git a/resources/css/etc/layout.css b/resources/css/etc/layout.css index a5f87a9..db1ab3d 100644 --- a/resources/css/etc/layout.css +++ b/resources/css/etc/layout.css @@ -91,14 +91,23 @@ main { /* main content title and actions */ > header { - @apply flex flex-row items-center justify-between px-6 2xl:px-8; + @apply grid items-center; + grid-template-columns: minmax(20rem, 20dvw) repeat(3, 1fr); h1 { - @apply h-12 max-h-12; + @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 flex flex-row items-center justify-end gap-2 h-12 max-h-12; + @apply col-span-2 flex flex-row items-center justify-end gap-2 pr-6 2xl:pr-8; } } @@ -125,7 +134,7 @@ main { @media (width >= 96rem) { /* 2xl */ main { body#app & { - grid-template-rows: 6rem auto; + grid-template-rows: 5.5rem auto; } } } diff --git a/resources/css/etc/theme.css b/resources/css/etc/theme.css index f734314..43e68bf 100644 --- a/resources/css/etc/theme.css +++ b/resources/css/etc/theme.css @@ -23,8 +23,25 @@ --color-cyan-400: oklch(92.6% 0.117 195.31); --color-cyan-500: oklch(90.54% 0.155 194.76); /* 00ffff */ --color-cyan-550: oklch(82% 0.2812 194.769); /* 00e3e3 */ + --color-cyan-600: oklch(80.43% 0.137 194.76); /* todo below */ + --color-cyan-700: oklch(70.28% 0.12 194.76); + --color-cyan-800: oklch(59.15% 0.101 194.76); + --color-cyan-900: oklch(49.05% 0.084 194.76); + --color-cyan-950: oklch(43.96% 0.075 194.76); + --color-magenta-50: oklch(96.93% 0.027 325.87); + --color-magenta-100: oklch(93.76% 0.056 326.06); + --color-magenta-200: oklch(87.58% 0.117 326.54); + --color-magenta-300: oklch(81.59% 0.181 327.09); + --color-magenta-400: oklch(75.66% 0.251 327.72); + --color-magenta-500: oklch(70.17% 0.322 328.37); + --color-magenta-550: oklch(0.666 0.3061 328.36); /* ee00ee */ + --color-magenta-600: oklch(62.55% 0.287 328.37); + --color-magenta-700: oklch(55.14% 0.253 328.37); + --color-magenta-800: oklch(47.69% 0.219 328.37); + --color-magenta-900: oklch(40.42% 0.186 328.37); + --color-magenta-950: oklch(36.79% 0.169 328.37); - --border-width-1.5: 1.5px; + --border-width-md: 1.5px; --radius-xs: 0.25rem; --radius-sm: 0.375rem; @@ -36,12 +53,18 @@ --radius-4xl: 3rem; --radius-blob: 80% 65% 90% 50% / 90% 80% 75% 75%; - --shadow-drop: 2.5px 2.5px 0 0 var(--color-primary); + --shadow-drop: 2.5px 2.5px 0 0 var(--color-primary); + --shadow-input: inset 0 0.25rem 0 0 var(--color-gray-100); + --spacing-md: 1.5px; --spacing-2px: 2px; + --text-2xs: 0.625rem; + --text-2xs--line-height: 1.2; + --text-2xl: 1.75rem; + --text-2xl--line-height: 1.333; --text-3xl: 2rem; - --text-3xl--line-height: calc(2.25 / 1.875); + --text-3xl--line-height: 1.2; --text-4xl: 3rem; --text-4xl--line-height: 1; } diff --git a/resources/css/etc/type.css b/resources/css/etc/type.css index 5fd935c..d5fa61a 100644 --- a/resources/css/etc/type.css +++ b/resources/css/etc/type.css @@ -24,6 +24,25 @@ font-style: normal; } +/* app name */ h1 { @apply font-serif text-3xl font-extrabold leading-tight; } + +/* page header */ +h2 { + @apply font-serif text-2xl font-extrabold leading-tight text-primary; +} + +/* links */ +a { + &.text { + @apply underline decoration-inherit underline-offset-2 text-magenta-600; + text-decoration-thickness: 1.5px; + transition: color 125ms ease-in-out; + + &:hover { + @apply text-magenta-700; + } + } +} diff --git a/resources/css/lib/accordion.css b/resources/css/lib/accordion.css new file mode 100644 index 0000000..9f79fb2 --- /dev/null +++ b/resources/css/lib/accordion.css @@ -0,0 +1,42 @@ +details { + + summary { + @apply relative flex items-center cursor-pointer list-none h-8 font-semibold z-0; + + &::-webkit-details-marker { + @apply hidden; + } + + &::before { + @apply block absolute top-0 left-0 -ml-3 -mt-1 bg-transparent rounded-md; + content: ''; + height: calc(100% + 0.5rem); + transition: background-color 100ms ease-in-out; + width: calc(100% + 1.5rem); + z-index: -1; + } + + &::after { + @apply block w-8 h-8 absolute right-0 top-0 bg-no-repeat bg-center; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E"); + content: ''; + transition: rotate 100ms ease-in-out; + } + + &:hover { + &::before { + @apply bg-gray-100; + } + } + } + + &[open] { + summary::after { + @apply rotate-180; + } + } + + > .content { + @apply mt-2; + } +} diff --git a/resources/css/lib/button.css b/resources/css/lib/button.css index e65a7c5..e155498 100644 --- a/resources/css/lib/button.css +++ b/resources/css/lib/button.css @@ -1,13 +1,13 @@ button, .button { @apply relative inline-flex items-center cursor-pointer gap-2 rounded-md h-11 px-4 text-lg font-medium; - transition: background-color 100ms ease-in-out; + transition: background-color 125ms ease-in-out; --button-border: var(--color-primary); --button-accent: var(--color-primary-hover); &.button--primary { - @apply bg-cyan-300; - border: 1.5px solid var(--button-border); + @apply bg-cyan-300 border-md border-solid; + border-color: var(--button-border); box-shadow: 2.5px 2.5px 0 0 var(--button-border); &:hover { @@ -31,3 +31,32 @@ button, } } } + +.button-group { + @apply relative flex flex-row items-center p-0 m-0 h-11 max-h-11; + + > label { + @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); + + > input[type="radio"] { + @apply hidden absolute top-0 left-0 w-0 h-0 max-w-0 max-h-0; + } + + &:first-child { + @apply rounded-l-md; + } + + &: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; + } + } +} diff --git a/resources/css/lib/calendar.css b/resources/css/lib/calendar.css new file mode 100644 index 0000000..8a181dd --- /dev/null +++ b/resources/css/lib/calendar.css @@ -0,0 +1,64 @@ +.calendar { + @apply grid col-span-3 pr-6 2xl:pr-8 pb-6 2xl:pb-8 pt-2; + grid-template-rows: 2rem 1fr; + + hgroup { + @apply grid grid-cols-7 w-full gap-1; + + > span { + @apply uppercase text-right pr-4 font-bold; + } + } + + ol { + @apply grid grid-cols-7 w-full gap-1; + contain: paint; + grid-auto-rows: 1fr; + + li { + @apply relative px-1 pt-8 border-t-md border-primary; + + &::before { + @apply absolute top-0 right-px w-auto h-8 flex items-center justify-end pr-4 text-sm font-medium; + content: attr(data-day-number); + } + + &.day--outside { + @apply bg-gray-50 text-gray-700; + } + + &.day--today { + @apply bg-cyan-100; + } + + &:nth-child(-n+7) { + @apply border-t-2; + } + + &:last-child { + @apply rounded-br-lg; + } + + .event { + @apply flex items-center text-xs gap-1 px-1 py-px font-medium truncate rounded-sm bg-transparent; + transition: background-color 125ms ease-in-out; + + .indicator { + --indicator-bg: var(--event-color); + } + + .title { + @apply grow; + } + + time { + @apply text-2xs; + } + + &:hover { + background-color: color-mix(in srgb, var(--event-color) 25%, #fff 100%); + } + } + } + } +} diff --git a/resources/css/lib/checkbox.css b/resources/css/lib/checkbox.css new file mode 100644 index 0000000..d5159cc --- /dev/null +++ b/resources/css/lib/checkbox.css @@ -0,0 +1,16 @@ +input[type="checkbox"] { + @apply border-md rounded-sm w-5 h-5 ring-0; + transition: border 150ms ease-in-out, + outline 150ms ease-in-out, + background 150ms ease-in-out, + box-shadow 200ms ease-out; + color: var(--checkbox-color); + border-color: var(--checkbox-color); + --checkbox-color: var(--color-primary); + + &:focus { + outline: 2px solid transparent; + outline-offset: 2px; + box-shadow: 0 0 0 2px #fff, 0 0 0 4px var(--checkbox-color), var(--tw-shadow); + } +} diff --git a/resources/css/lib/indicator.css b/resources/css/lib/indicator.css new file mode 100644 index 0000000..1552696 --- /dev/null +++ b/resources/css/lib/indicator.css @@ -0,0 +1,5 @@ +i.indicator { + @apply inline-flex w-2.5 h-2.5 min-w-2.5 min-h-2.5 rounded-full font-normal; + background-color: var(--indicator-bg); + --indicator-bg: var(--color-magenta-500); /* default color */ +} diff --git a/resources/css/lib/input.css b/resources/css/lib/input.css new file mode 100644 index 0000000..e9bb534 --- /dev/null +++ b/resources/css/lib/input.css @@ -0,0 +1,9 @@ +input[type="email"], +input[type="text"], +input[type="password"], +input[type="search"] { + @apply border-md border-gray-800 bg-white rounded-md shadow-input; + @apply focus:border-primary focus:ring-2 focus:ring-offset-2 focus:ring-cyan-600; + transition: box-shadow 125ms ease-in-out, + border-color 125ms ease-in-out; +} diff --git a/resources/css/lib/mini.css b/resources/css/lib/mini.css index 56be91c..ecb6cdb 100644 --- a/resources/css/lib/mini.css +++ b/resources/css/lib/mini.css @@ -12,10 +12,10 @@ /* days wrapper */ figure { - @apply border-1.5 border-primary shadow-drop rounded-md; + @apply border-md border-primary shadow-drop rounded-md; /* weekdays */ - figcaption { + hgroup { @apply grid grid-cols-7 p-2 pt-3 pb-0; span { @@ -55,7 +55,7 @@ &.day--with-events { &::after { - @apply absolute bottom-0 left-1/2 -translate-x-1/2 h-1 rounded-full w-4 bg-yellow-500; + @apply absolute bottom-0 left-1/2 -translate-x-1/2 h-1 rounded-full w-4 bg-magenta-500; content: ''; } &[data-event-count='1']::after { diff --git a/resources/svg/icons/chevron-down.svg b/resources/svg/icons/chevron-down.svg new file mode 100644 index 0000000..e576b4f --- /dev/null +++ b/resources/svg/icons/chevron-down.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/resources/svg/icons/chevron-left.svg b/resources/svg/icons/chevron-left.svg new file mode 100644 index 0000000..4828b06 --- /dev/null +++ b/resources/svg/icons/chevron-left.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/resources/svg/icons/chevron-right.svg b/resources/svg/icons/chevron-right.svg new file mode 100644 index 0000000..5db5d6e --- /dev/null +++ b/resources/svg/icons/chevron-right.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/resources/svg/icons/chevron-up.svg b/resources/svg/icons/chevron-up.svg new file mode 100644 index 0000000..58567c9 --- /dev/null +++ b/resources/svg/icons/chevron-up.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/resources/views/auth/login.blade.php b/resources/views/auth/login.blade.php index 5587322..19e3270 100644 --- a/resources/views/auth/login.blade.php +++ b/resources/views/auth/login.blade.php @@ -26,15 +26,15 @@
-
@if (Route::has('password.request')) - + {{ __('Forgot your password?') }} @endif diff --git a/resources/views/calendar/index.blade.php b/resources/views/calendar/index.blade.php index 7fa0079..cc85ca7 100644 --- a/resources/views/calendar/index.blade.php +++ b/resources/views/calendar/index.blade.php @@ -3,7 +3,18 @@

{{ __('Calendar') }}

+

+ {{ $active['month'] }} + {{ $active['year'] }} +

+
  • +
    + Day + Week + Month + 3-Up +
  • Create @@ -18,19 +29,23 @@ + diff --git a/resources/views/components/button/group-button.blade.php b/resources/views/components/button/group-button.blade.php new file mode 100644 index 0000000..29711ef --- /dev/null +++ b/resources/views/components/button/group-button.blade.php @@ -0,0 +1,11 @@ +@props([ + 'type' => 'submit', + 'name' => 'button-group', + 'value' => '1', + 'class' => '', + 'active' => false ]) + + diff --git a/resources/views/components/calendar/day.blade.php b/resources/views/components/calendar/day.blade.php new file mode 100644 index 0000000..e1f5df2 --- /dev/null +++ b/resources/views/components/calendar/day.blade.php @@ -0,0 +1,26 @@ +@props([ + 'day', // required + 'calendars' => [], // calendar palette keyed by id +]) + +
  • !empty($day['events']), + 'day--current' => $day['in_month'], + 'day--outside' => !$day['in_month'], + 'day--today' => $day['is_today'], +])> + @foreach ($day['events'] as $event) + @php + $bg = $calendars[(string) $event['calendar_id']]['color'] ?? '#999'; + @endphp + + + {{ $event['title'] }} + + + @endforeach +
  • diff --git a/resources/views/components/calendar/full.blade.php b/resources/views/components/calendar/full.blade.php new file mode 100644 index 0000000..be03f9b --- /dev/null +++ b/resources/views/components/calendar/full.blade.php @@ -0,0 +1,24 @@ +@props([ + 'grid' => ['weeks' => []], + 'calendars' => [], + 'class' => '' +]) + +
    +
    + Mon + Tue + Wed + Thu + Fri + Sat + Sun +
    +
      + @foreach ($grid['weeks'] as $week) + @foreach ($week as $day) + + @endforeach + @endforeach +
    +
    diff --git a/resources/views/components/calendar/mini.blade.php b/resources/views/components/calendar/mini.blade.php index f52402b..f13ed14 100644 --- a/resources/views/components/calendar/mini.blade.php +++ b/resources/views/components/calendar/mini.blade.php @@ -6,7 +6,7 @@ Controls
    -
    +
    U M T @@ -14,7 +14,7 @@ R F S -
    +
    {{ $slot }}
    diff --git a/resources/views/components/text-input.blade.php b/resources/views/components/text-input.blade.php index 51b722e..3302335 100644 --- a/resources/views/components/text-input.blade.php +++ b/resources/views/components/text-input.blade.php @@ -1,3 +1,3 @@ @props(['disabled' => false]) -merge(['class' => 'border-gray-300 focus:border-indigo-500 focus:ring-indigo-500 rounded-md shadow-xs']) }}> +merge(['class' => '']) }}> diff --git a/resources/views/layouts/guest.blade.php b/resources/views/layouts/guest.blade.php index db27856..1c84e67 100644 --- a/resources/views/layouts/guest.blade.php +++ b/resources/views/layouts/guest.blade.php @@ -14,7 +14,7 @@

    {{ config('app.name', 'Kithkin') }}

    -
    +
    {{ $slot }}