diff --git a/app/Http/Middleware/HandleAppearance.php b/app/Http/Middleware/HandleAppearance.php index 1a01684..de0b96b 100644 --- a/app/Http/Middleware/HandleAppearance.php +++ b/app/Http/Middleware/HandleAppearance.php @@ -16,7 +16,11 @@ class HandleAppearance */ public function handle(Request $request, Closure $next): Response { - View::share('appearance', $request->cookie('appearance') ?? 'system'); + $appearance = $request->cookie('appearance'); + $appearance = in_array($appearance, ['light', 'dark', 'system'], true) ? $appearance : 'system'; + + $request->attributes->set('appearance', $appearance); + View::share('appearance', $appearance); return $next($request); } diff --git a/app/Http/Middleware/HandleInertiaRequests.php b/app/Http/Middleware/HandleInertiaRequests.php index d43d301..d980988 100644 --- a/app/Http/Middleware/HandleInertiaRequests.php +++ b/app/Http/Middleware/HandleInertiaRequests.php @@ -46,6 +46,11 @@ class HandleInertiaRequests extends Middleware return [ ...parent::share($request), 'name' => config('app.name'), + 'appearance' => $request->attributes->get('appearance', 'system'), + 'localization' => [ + 'locale' => app()->getLocale(), + 'timeZone' => config('app.timezone'), + ], 'auth' => [ 'permissions' => [ 'canManageTeams' => $user !== null && ($this->permissionService->canManageAnyTeam($user) || $this->permissionService->allows($user, Permission::TeamCreate)), diff --git a/public/fonts/inter/InterVariable-Italic.woff2 b/public/fonts/inter/InterVariable-Italic.woff2 new file mode 100644 index 0000000..b3530f3 Binary files /dev/null and b/public/fonts/inter/InterVariable-Italic.woff2 differ diff --git a/public/fonts/inter/InterVariable.woff2 b/public/fonts/inter/InterVariable.woff2 new file mode 100644 index 0000000..5a8d3e7 Binary files /dev/null and b/public/fonts/inter/InterVariable.woff2 differ diff --git a/public/fonts/inter/LICENSE.txt b/public/fonts/inter/LICENSE.txt new file mode 100644 index 0000000..9b2ca37 --- /dev/null +++ b/public/fonts/inter/LICENSE.txt @@ -0,0 +1,92 @@ +Copyright (c) 2016 The Inter Project Authors (https://github.com/rsms/inter) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION AND CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/resources/css/app.css b/resources/css/app.css index 6f9d1f5..2949d41 100644 --- a/resources/css/app.css +++ b/resources/css/app.css @@ -1,56 +1,98 @@ @import 'tailwindcss'; - @import 'tw-animate-css'; +@import './theme.css'; @source '../../vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php'; @source '../../storage/framework/views/*.php'; +@source not '../../bootstrap/ssr'; -@custom-variant dark (&:is(.dark *)); +@custom-variant dark (&:where(.dark, .dark *)); + +@font-face { + font-family: Inter; + font-style: normal; + font-weight: 100 900; + font-display: swap; + src: url('/fonts/inter/InterVariable.woff2') format('woff2'); +} + +@font-face { + font-family: Inter; + font-style: italic; + font-weight: 100 900; + font-display: swap; + src: url('/fonts/inter/InterVariable-Italic.woff2') format('woff2'); +} @theme inline { - --font-sans: - Instrument Sans, ui-sans-serif, system-ui, sans-serif, - 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', - 'Noto Color Emoji'; + --font-sans: Inter, ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji'; - --radius-lg: var(--radius); - --radius-md: calc(var(--radius) - 2px); - --radius-sm: calc(var(--radius) - 4px); + --radius-sm: 0.125rem; + --radius-md: var(--radius); + --radius-lg: 0.5rem; + --radius-xl: 0.75rem; + + --spacing-page: 1.5rem; + --spacing-column: 1rem; + --spacing-card: 0.75rem; + --spacing-stack: 0.5rem; + --spacing-control: 2.75rem; + + --text-headline: 1.5rem; + --text-headline--line-height: 2rem; + --text-headline--font-weight: 600; + --text-headline--letter-spacing: -0.02em; + --text-headline-mobile: 1.25rem; + --text-headline-mobile--line-height: 1.75rem; + --text-headline-mobile--font-weight: 600; + --text-headline-mobile--letter-spacing: -0.02em; + --text-title: 1.125rem; + --text-title--line-height: 1.5rem; + --text-title--font-weight: 600; + --text-title--letter-spacing: -0.01em; + --text-body: 0.875rem; + --text-body--line-height: 1.25rem; + --text-caption: 0.8125rem; + --text-caption--line-height: 1.125rem; + --text-label: 0.6875rem; + --text-label--line-height: 1rem; + --text-label--font-weight: 700; + --text-label--letter-spacing: 0.05em; + + --shadow-card-hover: 0 4px 12px rgb(0 0 0 / 0.08); --color-background: var(--background); --color-foreground: var(--foreground); - --color-card: var(--card); --color-card-foreground: var(--card-foreground); - --color-popover: var(--popover); --color-popover-foreground: var(--popover-foreground); - --color-primary: var(--primary); --color-primary-foreground: var(--primary-foreground); - --color-secondary: var(--secondary); --color-secondary-foreground: var(--secondary-foreground); - --color-muted: var(--muted); --color-muted-foreground: var(--muted-foreground); - --color-accent: var(--accent); --color-accent-foreground: var(--accent-foreground); - --color-destructive: var(--destructive); --color-destructive-foreground: var(--destructive-foreground); - --color-border: var(--border); --color-input: var(--input); --color-ring: var(--ring); - + --color-brand: var(--brand); + --color-brand-foreground: var(--brand-foreground); + --color-success: var(--success); + --color-success-foreground: var(--success-foreground); + --color-warning: var(--warning); + --color-warning-foreground: var(--warning-foreground); + --color-info: var(--info); + --color-info-foreground: var(--info-foreground); --color-chart-1: var(--chart-1); --color-chart-2: var(--chart-2); --color-chart-3: var(--chart-3); --color-chart-4: var(--chart-4); --color-chart-5: var(--chart-5); - --color-sidebar: var(--sidebar-background); --color-sidebar-foreground: var(--sidebar-foreground); --color-sidebar-primary: var(--sidebar-primary); @@ -61,112 +103,35 @@ --color-sidebar-ring: var(--sidebar-ring); } -/* - The default border color has changed to `currentColor` in Tailwind CSS v4, - so we've added these compatibility styles to make sure everything still - looks the same as it did with Tailwind CSS v3. - - If we ever want to remove these styles, we need to add an explicit border - color utility to any element that depends on these defaults. -*/ -@layer base { - *, - ::after, - ::before, - ::backdrop, - ::file-selector-button { - border-color: var(--color-gray-200, currentColor); - } -} - -@layer utilities { - body, - html { - --font-sans: - 'Instrument Sans', ui-sans-serif, system-ui, sans-serif, - 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', - 'Noto Color Emoji'; - } -} - -:root { - --background: hsl(0 0% 100%); - --foreground: hsl(0 0% 3.9%); - --card: hsl(0 0% 100%); - --card-foreground: hsl(0 0% 3.9%); - --popover: hsl(0 0% 100%); - --popover-foreground: hsl(0 0% 3.9%); - --primary: hsl(0 0% 9%); - --primary-foreground: hsl(0 0% 98%); - --secondary: hsl(0 0% 92.1%); - --secondary-foreground: hsl(0 0% 9%); - --muted: hsl(0 0% 96.1%); - --muted-foreground: hsl(0 0% 45.1%); - --accent: hsl(0 0% 96.1%); - --accent-foreground: hsl(0 0% 9%); - --destructive: hsl(0 84.2% 60.2%); - --destructive-foreground: hsl(0 0% 98%); - --border: hsl(0 0% 92.8%); - --input: hsl(0 0% 89.8%); - --ring: hsl(0 0% 3.9%); - --chart-1: hsl(12 76% 61%); - --chart-2: hsl(173 58% 39%); - --chart-3: hsl(197 37% 24%); - --chart-4: hsl(43 74% 66%); - --chart-5: hsl(27 87% 67%); - --radius: 0.5rem; - --sidebar-background: hsl(0 0% 98%); - --sidebar-foreground: hsl(240 5.3% 26.1%); - --sidebar-primary: hsl(0 0% 10%); - --sidebar-primary-foreground: hsl(0 0% 98%); - --sidebar-accent: hsl(0 0% 94%); - --sidebar-accent-foreground: hsl(0 0% 30%); - --sidebar-border: hsl(0 0% 91%); - --sidebar-ring: hsl(217.2 91.2% 59.8%); - --sidebar: hsl(0 0% 98%); -} - -.dark { - --background: hsl(0 0% 3.9%); - --foreground: hsl(0 0% 98%); - --card: hsl(0 0% 3.9%); - --card-foreground: hsl(0 0% 98%); - --popover: hsl(0 0% 3.9%); - --popover-foreground: hsl(0 0% 98%); - --primary: hsl(0 0% 98%); - --primary-foreground: hsl(0 0% 9%); - --secondary: hsl(0 0% 14.9%); - --secondary-foreground: hsl(0 0% 98%); - --muted: hsl(0 0% 16.08%); - --muted-foreground: hsl(0 0% 63.9%); - --accent: hsl(0 0% 14.9%); - --accent-foreground: hsl(0 0% 98%); - --destructive: hsl(0 84% 60%); - --destructive-foreground: hsl(0 0% 98%); - --border: hsl(0 0% 14.9%); - --input: hsl(0 0% 14.9%); - --ring: hsl(0 0% 83.1%); - --chart-1: hsl(220 70% 50%); - --chart-2: hsl(160 60% 45%); - --chart-3: hsl(30 80% 55%); - --chart-4: hsl(280 65% 60%); - --chart-5: hsl(340 75% 55%); - --sidebar-background: hsl(0 0% 7%); - --sidebar-foreground: hsl(0 0% 95.9%); - --sidebar-primary: hsl(360, 100%, 100%); - --sidebar-primary-foreground: hsl(0 0% 100%); - --sidebar-accent: hsl(0 0% 15.9%); - --sidebar-accent-foreground: hsl(240 4.8% 95.9%); - --sidebar-border: hsl(0 0% 15.9%); - --sidebar-ring: hsl(217.2 91.2% 59.8%); - --sidebar: hsl(240 5.9% 10%); -} - @layer base { * { - @apply border-border outline-ring/50; + @apply border-border; } + body { - @apply bg-background text-foreground; + @apply bg-background font-sans text-body text-foreground antialiased; + font-optical-sizing: auto; + } + + :focus-visible { + outline: 2px solid var(--ring); + outline-offset: 3px; + } + + ::selection { + background: var(--brand); + color: var(--brand-foreground); + } +} + +/* Preserve completion events while removing visible nonessential motion. */ +@media (prefers-reduced-motion: reduce) { + *, + *::before, + *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; } } diff --git a/resources/css/theme.css b/resources/css/theme.css new file mode 100644 index 0000000..b7e8449 --- /dev/null +++ b/resources/css/theme.css @@ -0,0 +1,96 @@ +/* Fluid Emerald / Kinetic Kanban: shared by the first paint and the app bundle. */ +:root { + color-scheme: light; + --radius: 0.25rem; + --background: #f7f9fb; + --foreground: #191c1e; + --card: #ffffff; + --card-foreground: #191c1e; + --popover: #ffffff; + --popover-foreground: #191c1e; + --primary: #006e2f; + --primary-foreground: #ffffff; + --secondary: #d0e1fb; + --secondary-foreground: #38485d; + --muted: #f2f4f6; + --muted-foreground: #505f76; + --accent: #dcfce7; + --accent-foreground: #005321; + --destructive: #ba1a1a; + --destructive-foreground: #ffffff; + --border: #e2e8f0; + --input: #6d7b6c; + --ring: #006e2f; + --brand: #22c55e; + --brand-foreground: #002109; + --success: #dcfce7; + --success-foreground: #005321; + --warning: #fef3c7; + --warning-foreground: #78350f; + --info: #dbeafe; + --info-foreground: #1e40af; + --chart-1: #006e2f; + --chart-2: #505f76; + --chart-3: #9e4036; + --chart-4: #92400e; + --chart-5: #1d4ed8; + --sidebar-background: #f2f4f6; + --sidebar-foreground: #191c1e; + --sidebar-primary: #006e2f; + --sidebar-primary-foreground: #ffffff; + --sidebar-accent: #dcfce7; + --sidebar-accent-foreground: #005321; + --sidebar-border: #e2e8f0; + --sidebar-ring: #006e2f; + --sidebar: #f2f4f6; +} + +.dark { + color-scheme: dark; + --background: #0f172a; + --foreground: #f1f5f9; + --card: #1e293b; + --card-foreground: #f1f5f9; + --popover: #1e293b; + --popover-foreground: #f1f5f9; + --primary: #4ae176; + --primary-foreground: #002109; + --secondary: #334155; + --secondary-foreground: #e2e8f0; + --muted: #1e293b; + --muted-foreground: #b7c8e1; + --accent: #173b2b; + --accent-foreground: #6bff8f; + --destructive: #fca5a5; + --destructive-foreground: #450a0a; + --border: #334155; + --input: #8293aa; + --ring: #4ae176; + --brand: #22c55e; + --brand-foreground: #002109; + --success: #173b2b; + --success-foreground: #6bff8f; + --warning: #422006; + --warning-foreground: #fde68a; + --info: #172554; + --info-foreground: #bfdbfe; + --chart-1: #4ae176; + --chart-2: #b7c8e1; + --chart-3: #ffb4a9; + --chart-4: #fbbf24; + --chart-5: #60a5fa; + --sidebar-background: #111c30; + --sidebar-foreground: #f1f5f9; + --sidebar-primary: #4ae176; + --sidebar-primary-foreground: #002109; + --sidebar-accent: #173b2b; + --sidebar-accent-foreground: #6bff8f; + --sidebar-border: #334155; + --sidebar-ring: #4ae176; + --sidebar: #111c30; +} + +html { + background-color: var(--background); + color: var(--foreground); +} diff --git a/resources/js/app.ts b/resources/js/app.ts index c098682..1048d74 100644 --- a/resources/js/app.ts +++ b/resources/js/app.ts @@ -3,7 +3,8 @@ import { resolvePageComponent } from 'laravel-vite-plugin/inertia-helpers'; import type { DefineComponent } from 'vue'; import { createApp, h } from 'vue'; import '../css/app.css'; -import { initializeTheme } from '@/composables/useAppearance'; +import { createAppearance } from '@/composables/useAppearance'; +import { createLocalization } from '@/lib/localization'; const appName = import.meta.env.VITE_APP_NAME || 'Laravel'; @@ -17,12 +18,11 @@ createInertiaApp({ setup({ el, App, props, plugin }) { createApp({ render: () => h(App, props) }) .use(plugin) + .use(createLocalization(props.initialPage.props.localization)) + .use(createAppearance(props.initialPage.props.appearance)) .mount(el); }, progress: { - color: '#4B5563', + color: 'var(--primary)', }, }); - -// This will set light / dark mode on page load... -initializeTheme(); diff --git a/resources/js/components/AlertError.vue b/resources/js/components/AlertError.vue index c05937d..3849851 100644 --- a/resources/js/components/AlertError.vue +++ b/resources/js/components/AlertError.vue @@ -2,15 +2,15 @@ import { AlertCircle } from 'lucide-vue-next'; import { computed } from 'vue'; import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; +import { useLocalization } from '@/composables/useLocalization'; type Props = { errors: string[]; title?: string; }; -const props = withDefaults(defineProps(), { - title: 'Something went wrong.', -}); +const props = defineProps(); +const { t } = useLocalization(); const uniqueErrors = computed(() => Array.from(new Set(props.errors))); @@ -18,7 +18,7 @@ const uniqueErrors = computed(() => Array.from(new Set(props.errors)));