From b7282865c842266562db1240b8b1fd271bbfd0c9 Mon Sep 17 00:00:00 2001 From: Andrew Gioia Date: Thu, 15 Jan 2026 16:14:00 -0500 Subject: [PATCH] Long overdue changes to various components, migrations for address and location improvements, new address form and suggestion vies, profile components, and significant model cleanups --- .env.example | 4 +- app/Http/Controllers/EventController.php | 185 +- app/Http/Controllers/LocationController.php | 35 + app/Http/Controllers/ProfileController.php | 120 +- app/Models/Calendar.php | 28 +- app/Models/EventMeta.php | 14 +- app/Models/Location.php | 99 + app/Models/User.php | 19 + app/Models/UserAddress.php | 97 + app/Services/Location/Geocoder.php | 237 ++- config/services.php | 51 +- ..._20_000001_add_place_name_to_locations.php | 23 + ...08_21_000000_user_address_improvements.php | 79 + package-lock.json | 1808 ++++++++++++++++- package.json | 1 + resources/css/etc/layout.css | 2 +- resources/js/app.js | 8 +- .../views/calendar/settings/index.blade.php | 2 +- resources/views/calendar/show.blade.php | 6 +- .../views/components/button/icon.blade.php | 21 +- .../views/components/button/index.blade.php | 21 +- .../views/components/calendar/day.blade.php | 4 +- .../profile/settings-menu.blade.php | 28 + resources/views/event/form.blade.php | 35 +- .../views/event/partials/details.blade.php | 2 +- .../event/partials/suggestions.blade.php | 39 + resources/views/event/show.blade.php | 2 +- resources/views/layouts/navigation.blade.php | 30 +- resources/views/profile/edit.blade.php | 9 + resources/views/profile/index.blade.php | 30 + .../profile/partials/addresses-form.blade.php | 153 ++ routes/web.php | 59 +- 32 files changed, 3038 insertions(+), 213 deletions(-) create mode 100644 app/Http/Controllers/LocationController.php create mode 100644 app/Models/UserAddress.php create mode 100644 database/migrations/2025_08_20_000001_add_place_name_to_locations.php create mode 100644 database/migrations/2025_08_21_000000_user_address_improvements.php create mode 100644 resources/views/components/profile/settings-menu.blade.php create mode 100644 resources/views/event/partials/suggestions.blade.php create mode 100644 resources/views/profile/index.blade.php create mode 100644 resources/views/profile/partials/addresses-form.blade.php diff --git a/.env.example b/.env.example index 76e6173..b73105f 100644 --- a/.env.example +++ b/.env.example @@ -19,7 +19,9 @@ ADMIN_LASTNAME=Admin GEOCODER=arcgis GEOCODER_USER_AGENT="Kithkin/1.0 (amrou@kithk.in)" GEOCODER_TIMEOUT=30 -GEOCIDER_EMAIL=amrou@kithk.in +GEOCODER_EMAIL=amrou@kithk.in +GEOCODER_COUNTRY=USA +GEOCODER_CATEGORIES=POI,Address ARCGIS_API_KEY= ARCGIS_STORE_RESULTS=true # set to false to not store results diff --git a/app/Http/Controllers/EventController.php b/app/Http/Controllers/EventController.php index e7a90fc..0ccc78c 100644 --- a/app/Http/Controllers/EventController.php +++ b/app/Http/Controllers/EventController.php @@ -2,41 +2,73 @@ namespace App\Http\Controllers; -use Illuminate\Http\Request; -use Illuminate\Support\Str; -use Carbon\Carbon; -use Illuminate\Support\Facades\DB; use App\Models\Calendar; use App\Models\Event; use App\Models\EventMeta; +use App\Models\Location; +use App\Services\Location\Geocoder; +use Carbon\Carbon; +use Illuminate\Http\Request; +use Illuminate\Support\Str; +use Illuminate\Support\Facades\DB; +use Illuminate\Support\Facades\Log; class EventController extends Controller { /** + * * create a new event page */ - public function create(Calendar $calendar) + public function create(Calendar $calendar, Request $request) { + // authorize access to this calendar $this->authorize('update', $calendar); + // the instance for the signed-in user (provides the uri/slug) $instance = $calendar->instanceForUser(); + $slug = $instance?->uri ?? $calendar->id; // fallback just in case + + // build a fresh event "shell" with meta defaults (keeps your view happy) $event = new Event; $event->meta = (object) [ - 'title' => '', + 'title' => '', 'description' => '', - 'location' => '', - 'start_at' => null, - 'end_at' => null, - 'all_day' => false, - 'category' => '', + 'location' => '', + 'start_at' => null, + 'end_at' => null, + 'all_day' => false, + 'category' => '', ]; - $start = $event->start_at; - $end = $event->end_at; - return view('event.form', compact('calendar', 'instance', 'event', 'start', 'end')); + // choose a timezone and derive defaults for start/end + $tz = auth()->user()->timezone ?? config('app.timezone', 'UTC'); + + // if ?date=YYYY-MM-DD is present, start that day at 9am; otherwise "now" + $anchor = $request->query('date') + ? Carbon::parse($request->query('date'), $tz)->startOfDay()->addHours(9) + : Carbon::now($tz); + + $anchor->second(0); + + $start_carbon = $anchor->copy(); + $end_carbon = $anchor->copy()->addHour(); + + // format for + $start = $start_carbon->format('Y-m-d\TH:i'); + $end = $end_carbon->format('Y-m-d\TH:i'); + + return view('event.form', compact( + 'calendar', // bound model (so route() can take the model directly) + 'instance', // convenience in the view + 'slug', // if you prefer passing just the slug into route() + 'event', + 'start', + 'end' + )); } /** + * * edit event page */ public function edit(Calendar $calendar, Event $event) @@ -58,6 +90,7 @@ class EventController extends Controller } /** + * * single event view handling * * URL: /calendar/{uuid}/event/{event_id} @@ -72,8 +105,9 @@ class EventController extends Controller // authorize $this->authorize('view', $event); - // eager-load meta so the view has everything + // eager-load metadata so the view has everything $event->load('meta'); + $event->load('meta.venue'); // check for HTML; it sends `HX-Request: true` on every AJAX call $isHtmx = $request->header('HX-Request') === 'true'; @@ -90,7 +124,7 @@ class EventController extends Controller return $isHtmx ? view('event.partials.details', $data) // tiny fragment for the modal - : view('event.show', $data); // full-page fallback + : view('event.show', $data); // full-page fallback } @@ -100,9 +134,10 @@ class EventController extends Controller */ /** + * * insert vevent into sabre’s calendarobjects + meta row */ - public function store(Request $req, Calendar $calendar) + public function store(Request $req, Calendar $calendar, Geocoder $geocoder) { $this->authorize('update', $calendar); @@ -114,42 +149,49 @@ class EventController extends Controller 'location' => 'nullable|string', 'all_day' => 'sometimes|boolean', 'category' => 'nullable|string|max:50', + + // normalized fields from the suggestions ui (all optional) + 'loc_display_name' => 'nullable|string', + 'loc_place_name' => 'nullable|string', // optional if you add this hidden input + 'loc_street' => 'nullable|string', + 'loc_city' => 'nullable|string', + 'loc_state' => 'nullable|string', + 'loc_postal' => 'nullable|string', + 'loc_country' => 'nullable|string', + 'loc_lat' => 'nullable', + 'loc_lon' => 'nullable', ]); - // prepare payload - $uid = Str::uuid() . '@' . parse_url(config('app.url'), PHP_URL_HOST); + $uid = Str::uuid() . '@' . parse_url(config('app.url'), PHP_URL_HOST); - // store events as UTC in the database; convert to calendar time in the view - $client_timezone = $calendar->timezone ?? 'UTC'; - $start = Carbon::createFromFormat('Y-m-d\TH:i', $data['start_at'], $client_timezone) - ->setTimezone('UTC'); - $end = Carbon::createFromFormat('Y-m-d\TH:i', $data['end_at'], $client_timezone) - ->setTimezone('UTC'); + // parse local -> utc + $clientTz = $calendar->timezone ?? 'UTC'; + $start = Carbon::createFromFormat('Y-m-d\TH:i', $data['start_at'], $clientTz)->utc(); + $end = Carbon::createFromFormat('Y-m-d\TH:i', $data['end_at'], $clientTz)->utc(); - // prepare strings - $description = $data['description'] ?? ''; - $location = $data['location'] ?? ''; - $description = str_replace("\n", '\\n', $description); - $location = str_replace("\n", '\\n', $location); + // normalize description/location for ics + $description = str_replace("\n", '\\n', $data['description'] ?? ''); + $locationStr = str_replace("\n", '\\n', $data['location'] ?? ''); - /* build minimal iCalendar payload */ + // write dtstart/dtend as utc with "Z" $ical = <<utc()->format('Ymd\\THis\\Z')} -DTSTART:{$start->format('Ymd\\THis')} -DTEND:{$end->format('Ymd\\THis')} -SUMMARY:{$data['title']} -DESCRIPTION:$description -LOCATION:$location -END:VEVENT -END:VCALENDAR + BEGIN:VCALENDAR + VERSION:2.0 + PRODID:-//Kithkin//Laravel CalDAV//EN + BEGIN:VEVENT + UID:$uid + DTSTAMP:{$start->format('Ymd\THis\Z')} + DTSTART:{$start->format('Ymd\THis\Z')} + DTEND:{$end->format('Ymd\THis\Z')} + SUMMARY:{$data['title']} + DESCRIPTION:$description + LOCATION:$locationStr + END:VEVENT + END:VCALENDAR -ICS; + ICS; + // create sabre object $event = Event::create([ 'calendarid' => $calendar->id, 'uri' => Str::uuid() . '.ics', @@ -161,11 +203,59 @@ ICS; 'calendardata' => $ical, ]); + // resolve a location_id + $locationId = null; + $raw = $data['location'] ?? null; + + // did the user pick a suggestion (hidden normalized fields present)? + $hasNormHints = $req->filled('loc_display_name') || + $req->filled('loc_place_name') || + $req->filled('loc_street') || + $req->filled('loc_city') || + $req->filled('loc_state') || + $req->filled('loc_postal') || + $req->filled('loc_country') || + $req->filled('loc_lat') || + $req->filled('loc_lon'); + + if ($raw) { + if ($hasNormHints) { + $norm = [ + 'display_name' => $req->input('loc_display_name') ?: $raw, + 'place_name' => $req->input('loc_place_name'), // fine if null + 'raw_address' => $raw, + 'street' => $req->input('loc_street'), + 'city' => $req->input('loc_city'), + 'state' => $req->input('loc_state'), + 'postal' => $req->input('loc_postal'), + 'country' => $req->input('loc_country'), + 'lat' => $req->filled('loc_lat') ? (float) $req->input('loc_lat') : null, + 'lon' => $req->filled('loc_lon') ? (float) $req->input('loc_lon') : null, + ]; + $loc = Location::findOrCreateNormalized($norm, $raw); + $locationId = $loc->id; + + } else { + // no hints: try geocoding the free-form string + $norm = $geocoder->forward($raw); + if ($norm) { + $loc = Location::findOrCreateNormalized($norm, $raw); + $locationId = $loc->id; + } else { + // label-only fallback so the event still links to a location row + $loc = Location::labelOnly($raw); + $locationId = $loc->id; + } + } + } + + // meta row (store raw string and link to normalized location if we have one) $event->meta()->create([ 'title' => $data['title'], 'description' => $data['description'] ?? null, - 'location' => $data['location'] ?? null, - 'all_day' => $data['all_day'] ?? false, + 'location' => $raw, + 'location_id' => $locationId, + 'all_day' => (bool) ($data['all_day'] ?? false), 'category' => $data['category'] ?? null, 'start_at' => $start, 'end_at' => $end, @@ -175,6 +265,7 @@ ICS; } /** + * * update vevent + meta */ public function update(Request $req, Calendar $calendar, Event $event) diff --git a/app/Http/Controllers/LocationController.php b/app/Http/Controllers/LocationController.php new file mode 100644 index 0000000..7e0a757 --- /dev/null +++ b/app/Http/Controllers/LocationController.php @@ -0,0 +1,35 @@ +input('q', $request->input('location', ''))); + + // short queries: return empty list (avoid rate limits) + if ($q === '' || mb_strlen($q) < 3) { + return response()->view('event.partials.suggestions', [ + 'suggestions' => [], + ]); + } + + try { + // pass the current user so the geocoder can bias by zip/centroid + // signature: suggestions(string $query, int $limit = 5, ?User $user = null) + $suggestions = $geo->suggestions($q, 5, $request->user()); + } catch (\Throwable $e) { + Log::warning('location suggest failed', ['q' => $q, 'error' => $e->getMessage()]); + $suggestions = []; + } + + return view('event.partials.suggestions', [ + 'suggestions' => $suggestions, + ]); + } +} diff --git a/app/Http/Controllers/ProfileController.php b/app/Http/Controllers/ProfileController.php index a48eb8d..4358bb6 100644 --- a/app/Http/Controllers/ProfileController.php +++ b/app/Http/Controllers/ProfileController.php @@ -3,22 +3,41 @@ namespace App\Http\Controllers; use App\Http\Requests\ProfileUpdateRequest; +use App\Models\UserAddress; +use App\Models\Location; +use App\Services\Location\Geocoder; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; +use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Redirect; use Illuminate\View\View; class ProfileController extends Controller { /** - * Display the user's profile form. + * profile index test + */ + public function index() + { + return $this->frame('profile.partials.addresses-form'); + } + + /** + * display user's profile forms */ public function edit(Request $request): View { - return view('profile.edit', [ - 'user' => $request->user(), - ]); + $user = $request->user(); + + // try primary of each kind; fall back to the first of that kind + $home = $user->addresses()->where('kind', 'home')->where('is_primary', 1)->first() + ?? $user->addresses()->where('kind', 'home')->first(); + + $billing = $user->addresses()->where('kind', 'billing')->where('is_primary', 1)->first() + ?? $user->addresses()->where('kind', 'billing')->first(); + + return view('profile.edit', compact('user', 'home', 'billing')); } /** @@ -37,6 +56,87 @@ class ProfileController extends Controller return Redirect::route('profile.edit')->with('status', 'profile-updated'); } + /** + * save or update the user's addresses (/profile/addresses) + */ + public function saveAddresses(Request $request, Geocoder $geocoder): RedirectResponse + { + $user = $request->user(); + + $data = $request->validate([ + 'home' => 'array', + 'home.label' => 'nullable|string|max:100', + 'home.line1' => 'nullable|string|max:255', + 'home.line2' => 'nullable|string|max:255', + 'home.city' => 'nullable|string|max:120', + 'home.state' => 'nullable|string|max:64', + 'home.postal' => 'nullable|string|max:32', + 'home.country' => 'nullable|string|max:64', + 'home.phone' => 'nullable|string|max:32', + + 'billing' => 'array', + 'billing.label' => 'nullable|string|max:100', + 'billing.line1' => 'nullable|string|max:255', + 'billing.line2' => 'nullable|string|max:255', + 'billing.city' => 'nullable|string|max:120', + 'billing.state' => 'nullable|string|max:64', + 'billing.postal' => 'nullable|string|max:32', + 'billing.country' => 'nullable|string|max:64', + 'billing.phone' => 'nullable|string|max:32', + ]); + + DB::transaction(function () use ($user, $data, $geocoder) { + + $save = function (string $kind, array $payload) use ($user, $geocoder) { + // short-circuit if nothing filled + $filled = collect($payload)->only(['line1','line2','city','state','postal','country','phone']) + ->filter(fn ($v) => filled($v)); + if ($filled->isEmpty()) { + return; + } + + // upsert a single primary address of this kind + $addr = UserAddress::updateOrCreate( + ['user_id' => $user->id, 'kind' => $kind, 'is_primary' => 1], + [ + 'label' => $payload['label'] ?? ucfirst($kind), + 'line1' => $payload['line1'] ?? null, + 'line2' => $payload['line2'] ?? null, + 'city' => $payload['city'] ?? null, + 'state' => $payload['state'] ?? null, + 'postal' => $payload['postal'] ?? null, + 'country' => $payload['country'] ?? null, + 'phone' => $payload['phone'] ?? null, + ] + ); + + // build a singleLine string to geocode + $singleLine = collect([ + $addr->line1, $addr->line2, $addr->city, $addr->state, $addr->postal, $addr->country + ])->filter()->implode(', '); + + if ($singleLine !== '') { + // geocode and link to locations + $norm = $geocoder->forward($singleLine); + if ($norm) { + $loc = Location::findOrCreateNormalized($norm, $singleLine); + $addr->location_id = $loc->id; + $addr->save(); + } + } + }; + + if (isset($data['home'])) { + $save('home', $data['home']); + } + if (isset($data['billing'])) { + $save('billing', $data['billing']); + } + }); + + return Redirect::route('profile.edit')->with('status', 'addresses-updated'); + } + /** * Delete the user's account. */ @@ -57,4 +157,16 @@ class ProfileController extends Controller return Redirect::to('/'); } + + /** + * content frame handler + */ + private function frame(?string $view = null, array $data = []) + { + return view('profile.index', [ + 'view' => $view, + 'data' => $data, + ]); + } + } diff --git a/app/Models/Calendar.php b/app/Models/Calendar.php index 96feb52..bdae5cf 100644 --- a/app/Models/Calendar.php +++ b/app/Models/Calendar.php @@ -40,16 +40,17 @@ class Calendar extends Model } /* get the primary? instance for a user */ - public function instanceForUser(?User $user = null) + public function instanceForUser(?User $user = null): CalendarInstance { $user = $user ?? auth()->user(); return $this->instances() - ->where('principaluri', 'principals/' . $user->email) + ->where('principaluri', $user->principal_uri) ->first(); } /** + * inbound urls * convert "/calendar/{slug}" into the correct calendar instance (uri column) * * @param mixed $value The URI segment (instance UUID). @@ -57,11 +58,28 @@ class Calendar extends Model */ public function resolveRouteBinding($value, $field = null): mixed { - return $this->whereHas('instances', function (Builder $q) use ($value) { + $user = Auth::user(); + + return $this->newQuery() + ->whereHas('instances', function (Builder $q) use ($value, $user) { $q->where('uri', $value) - ->where('principaluri', Auth::user()->principal_uri); + ->where('principaluri', $user->principal_uri); }) - ->with('instances') + ->with(['instances' => function ($q) use ($user) { + $q->where('principaluri', $user->principal_uri); + }]) ->firstOrFail(); } + /** + * outbound urls + */ + public function getRouteKey(): string + { + // use the per-user instance slug for URLs; fall back to id if not available + $instance = $this->instances() + ->where('principaluri', Auth::user()->principal_uri) + ->first(); + + return $instance->uri ?? (string) $this->getKey(); + } } diff --git a/app/Models/EventMeta.php b/app/Models/EventMeta.php index 960ab44..714669e 100644 --- a/app/Models/EventMeta.php +++ b/app/Models/EventMeta.php @@ -33,8 +33,6 @@ class EventMeta extends Model 'extra' => 'array', ]; - - /** * convenience wrapper that mimics an “UPSERT” for meta rows. * @@ -53,7 +51,6 @@ class EventMeta extends Model } /** - * * relationships */ @@ -63,9 +60,16 @@ class EventMeta extends Model return $this->belongsTo(Event::class, 'event_id'); } - /* back-reference to location record */ - public function location(): BelongsTo + /* back-reference to location record; calling this venue so that it doesn't shadow the "location" column */ + public function venue(): BelongsTo { return $this->belongsTo(Location::class, 'location_id'); } + + /* convenient, safe label to use in views */ + public function getLocationLabelAttribute(): string + { + // prefer normalized place name; otherwise fall back to the raw string column + return $this->venue?->short_label ?? ($this->location ?? ''); + } } diff --git a/app/Models/Location.php b/app/Models/Location.php index 5500721..628e023 100644 --- a/app/Models/Location.php +++ b/app/Models/Location.php @@ -10,6 +10,7 @@ class Location extends Model protected $fillable = [ 'display_name', + 'place_name', 'raw_address', 'street', 'city', @@ -26,4 +27,102 @@ class Location extends Model 'lat' => 'float', 'lon' => 'float', ]; + + /** + * find an existing location by its normalized components or create it. + * backfills lat/lon/raw_address/place_name if the found row is missing them. + */ + public static function findOrCreateNormalized(array $norm, ?string $raw = null): self + { + $lookup = [ + 'display_name' => $norm['display_name'] ?? null, + 'street' => $norm['street'] ?? null, + 'city' => $norm['city'] ?? null, + 'state' => $norm['state'] ?? null, + 'postal' => $norm['postal'] ?? null, + 'country' => $norm['country'] ?? null, + ]; + + $existing = static::where($lookup)->first(); + + // fallback: try matching by the raw label (useful for seeds like "Home") + if (!$existing && $raw) { + $existing = static::where('display_name', $raw) + ->orWhere('raw_address', $raw) + ->first(); + } + + if ($existing) { + $changed = false; + + // backfill coords if missing (and we have them) + $hasNewCoords = (!empty($norm['lat']) || !empty($norm['lon'])); + if (($existing->lat === null || $existing->lon === null) && $hasNewCoords) { + $existing->lat = $norm['lat'] ?? $existing->lat; + $existing->lon = $norm['lon'] ?? $existing->lon; + $changed = true; + } + + // backfill raw address if missing + if ($raw && $existing->raw_address === null) { + $existing->raw_address = $raw; + $changed = true; + } + + // backfill place_name if missing + if ($existing->place_name === null && !empty($norm['place_name'])) { + $existing->place_name = $norm['place_name']; + $changed = true; + } + + if ($changed) { + $existing->save(); + } + + return $existing; + } + + return static::create([ + 'display_name' => $norm['display_name'] ?? ($raw ?: 'Unknown'), + 'place_name' => $norm['place_name'] ?? null, + 'raw_address' => $norm['raw_address'] ?? $raw, + 'street' => $norm['street'] ?? null, + 'city' => $norm['city'] ?? null, + 'state' => $norm['state'] ?? null, + 'postal' => $norm['postal'] ?? null, + 'country' => $norm['country'] ?? null, + 'lat' => $norm['lat'] ?? null, + 'lon' => $norm['lon'] ?? null, + ]); + } + + /** + * thin alias for backwards compatibility with older call-sites. + * prefer using findOrCreateNormalized() directly. + */ + public static function firstOrCreateNormalized(array $norm, ?string $raw = null): self + { + return static::findOrCreateNormalized($norm, $raw); + } + + /** + * create/find a label-only location (no geocode) + */ + public static function labelOnly(string $label): self + { + return static::firstOrCreate( + ['display_name' => $label], + ['raw_address' => null] + ); + } + + /** + * accessor: short label for ui (falls back gracefully) + */ + public function getShortLabelAttribute(): string + { + return $this->place_name + ?? $this->display_name + ?? trim(collect([$this->street, $this->city])->filter()->join(', ')); + } } diff --git a/app/Models/User.php b/app/Models/User.php index 7bd188d..464bded 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -69,4 +69,23 @@ class User extends Authenticatable { return 'principals/' . $this->email; } + + /** + * get all user addresses + */ + public function addresses() + { + return $this->hasMany(\App\Models\UserAddress::class, 'user_id', 'id'); + } + + /** + * get the user's billing address + */ + public function billingAddress() + { + return $this->addresses() + ->where('kind', 'billing') + ->where('is_primary', 1) + ->first(); + } } diff --git a/app/Models/UserAddress.php b/app/Models/UserAddress.php new file mode 100644 index 0000000..db33215 --- /dev/null +++ b/app/Models/UserAddress.php @@ -0,0 +1,97 @@ + 'boolean', + 'created_at' => 'datetime', + 'updated_at' => 'datetime', + ]; + + /* relationships */ + + // belongs to a user (users.id is char(26)) + public function user(): BelongsTo + { + return $this->belongsTo(User::class, 'user_id', 'id'); + } + + // optional normalized/geocoded location row + public function location(): BelongsTo + { + return $this->belongsTo(Location::class, 'location_id'); + } + + /* query scopes */ + + // filter by kind (e.g., 'billing', 'shipping', 'home', 'work') + public function scopeKind($query, string $kind) + { + return $query->where('kind', $kind); + } + + // filter to primary rows + public function scopePrimary($query) + { + return $query->where('is_primary', 1); + } + + /* helpers */ + + // set this address as the single primary of its kind for the user + public function setAsPrimary(): void + { + DB::transaction(function () { + // clear any existing primary for this user+kind (null is allowed by the unique index) + static::where('user_id', $this->user_id) + ->where('kind', $this->kind) + ->update(['is_primary' => null]); + + // mark this one as primary + $this->is_primary = 1; + $this->save(); + }); + } + + // quick one-line label useful for dropdowns and summaries + public function getOneLineAttribute(): string + { + $parts = array_filter([ + $this->label, + $this->line1, + $this->line2, + $this->city, + $this->state, + $this->postal, + $this->country, + ]); + + return trim(collect($parts)->join(', ')); + } +} diff --git a/app/Services/Location/Geocoder.php b/app/Services/Location/Geocoder.php index 587898c..3d43bae 100644 --- a/app/Services/Location/Geocoder.php +++ b/app/Services/Location/Geocoder.php @@ -2,51 +2,52 @@ namespace App\Services\Location; +use \App\Models\User; use Illuminate\Support\Arr; use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Log; class Geocoder { - public function __construct( - private array $cfg = [] - ) { - $this->cfg = config('services.geocoding'); + public function __construct(private array $cfg = []) + { + $this->cfg = config("services.geocoding"); } /** - * Forward geocode a free-form string. - * Returns a normalized array for your `locations` table or null. + * forward geocode with optional per-user bias */ - public function forward(string $query): ?array + public function forward(string $query, ?User $user = null): ?array { - $provider = $this->cfg['provider'] ?? 'arcgis'; - - // Treat obvious non-address labels as non-geocodable (e.g., "Home", "Office") if (mb_strlen(trim($query)) < 4) { return null; } - return match ($provider) { - 'arcgis' => $this->forwardArcgis($query), + return match ($this->cfg['provider'] ?? 'arcgis') { + 'arcgis' => $this->forwardArcgis($query, $this->biasForUser($user)), default => null, }; } /** - * Reverse geocode lon/lat → address. (Optional) + * reverse geocode lon/lat → address. (Optional) */ public function reverse(float $lat, float $lon): ?array { - $provider = $this->cfg['provider'] ?? 'arcgis'; - return match ($provider) { + return match ($this->cfg['provider'] ?? 'arcgis') { 'arcgis' => $this->reverseArcgis($lat, $lon), default => null, }; } - /* ---------------- ArcGIS World Geocoding ---------------- */ + /* + * + * ArcGIS World Geocoding + */ + /** + * request + */ private function http() { return Http::retry(3, 500) @@ -56,59 +57,139 @@ class Geocoder ]); } + /** + * get base url + */ private function arcgisBase(): string { - return rtrim($this->cfg['arcgis']['endpoint'], '/'); + return rtrim($this->cfg["arcgis"]["endpoint"], "/"); } - private function forwardArcgis(string $query): ?array + /** + * pull a bias from the user (zip -> centroid) and cache it + */ + private function biasForUser(?User $user): ?array { + if (!$user) { + return null; + } + + // if you store home_lat/home_lon on users, prefer those + if (!empty($user->home_lat) && !empty($user->home_lon)) { + return ['lat' => (float)$user->home_lat, 'lon' => (float)$user->home_lon, 'radius_km' => 120.0]; + } + + // else try user zip/postal + $zip = $user->postal_code ?? $user->zip ?? null; + if (!$zip) { + return null; + } + + $cacheKey = "geo:bias:zip:{$zip}"; + $bias = Cache::remember($cacheKey, now()->addDays(7), function () use ($zip) { + $a = $this->cfg['arcgis']; + + $params = [ + 'singleLine' => $zip, + 'category' => 'Postal', + 'maxLocations' => 1, + 'f' => 'pjson', + 'token' => $a['api_key'], + 'countryCode' => $a['country_code'] ?? null, + ]; + + $res = $this->http()->get($this->arcgisBase().'/findAddressCandidates', $params); + if (!$res->ok()) { + Log::info('arcgis zip bias lookup failed', ['zip' => $zip, 'status' => $res->status()]); + return null; + } + + $cand = Arr::first($res->json('candidates', [])); + if (!$cand || empty($cand['location'])) { + return null; + } + + return [ + 'lat' => (float)($cand['location']['y'] ?? 0), + 'lon' => (float)($cand['location']['x'] ?? 0), + 'radius_km' => 120.0, // reasonable city-scale radius + ]; + }); + + return $bias ?: null; + } + + /** + * compute a bounding box string from center + radius (km) + */ + private function bboxFromBias(float $lat, float $lon, float $radiusKm): string + { + $latDelta = $radiusKm / 111.0; + $lonDelta = $radiusKm / (111.0 * max(cos(deg2rad($lat)), 0.01)); + $minx = $lon - $lonDelta; + $miny = $lat - $latDelta; + $maxx = $lon + $lonDelta; + $maxy = $lat + $latDelta; + return "{$minx},{$miny},{$maxx},{$maxy}"; + } + + /** + * handle location request with optional bias array + */ + private function forwardArcgis(string $query, ?array $bias): ?array + { + $a = $this->cfg['arcgis']; + $params = [ - 'singleLine' => $query, - 'outFields' => $this->cfg['arcgis']['out_fields'] ?? '*', - 'maxLocations'=> $this->cfg['arcgis']['max_results'] ?? 1, - 'f' => 'pjson', - 'token' => $this->cfg['arcgis']['api_key'], + 'singleLine' => $query, + 'outFields' => $a['out_fields'] ?? '*', + 'maxLocations' => (int)($a['max_results'] ?? 5), + 'f' => 'pjson', + 'token' => $a['api_key'], + 'category' => $a['categories'] ?? 'POI,Address', + 'countryCode' => $a['country_code'] ?? null, ]; - // If your plan permits/requests it: - if (!empty($this->cfg['arcgis']['store'])) { - $params['forStorage'] = 'true'; + if ($bias && $bias['lat'] && $bias['lon']) { + $params['location'] = $bias['lon'].','.$bias['lat']; + if (!empty($bias['radius_km'])) { + $params['searchExtent'] = $this->bboxFromBias($bias['lat'], $bias['lon'], (float)$bias['radius_km']); + } } $res = $this->http()->get($this->arcgisBase().'/findAddressCandidates', $params); - if (!$res->ok()) { - Log::warning('ArcGIS forward geocode failed', ['status' => $res->status(), 'q' => $query]); + Log::warning('arcgis forward geocode failed', ['status' => $res->status(), 'q' => $query]); return null; } - $json = $res->json(); - $cand = Arr::first($json['candidates'] ?? []); - if (!$cand) { + $cands = $res->json('candidates', []); + if (!$cands) { return null; } - return $this->normalizeArcgisCandidate($cand, $query); + // simplest “best”: highest arcgis score + usort($cands, fn($a,$b) => ($b['score'] <=> $a['score'])); + $best = $cands[0]; + + return $this->normalizeArcgisCandidate($best, $query); } + /** + * lookup based on lat/lon + */ private function reverseArcgis(float $lat, float $lon): ?array { + $a = $this->cfg['arcgis']; $params = [ - // ArcGIS expects x=lon, y=lat 'location' => "{$lon},{$lat}", 'f' => 'pjson', - 'token' => $this->cfg['arcgis']['api_key'], + 'token' => $a['api_key'], ]; - if (!empty($this->cfg['arcgis']['store'])) { - $params['forStorage'] = 'true'; - } - $res = $this->http()->get($this->arcgisBase().'/reverseGeocode', $params); - if (!$res->ok()) { - Log::warning('ArcGIS reverse geocode failed', ['status' => $res->status()]); + Log::warning('arcgis reverse geocode failed', ['status' => $res->status()]); return null; } @@ -126,25 +207,21 @@ class Geocoder 'state' => $addr['Region'] ?? null, 'postal' => $addr['Postal'] ?? null, 'country' => $addr['CountryCode'] ?? null, - // reverseGeocode returns location as x/y too: 'lat' => Arr::get($j, 'location.y'), 'lon' => Arr::get($j, 'location.x'), ]; } + /** + * format the returned data + */ private function normalizeArcgisCandidate(array $c, string $query): array { - $loc = $c['location'] ?? []; - $attr = $c['attributes'] ?? []; + $loc = $c['location'] ?? []; + $attr = $c['attributes'] ?? []; - // Prefer LongLabel → address → Place_addr - $display = $attr['LongLabel'] - ?? $c['address'] - ?? $attr['Place_addr'] - ?? $query; - - // ArcGIS often returns both 'Address' and 'Place_addr'; use either. - $street = $attr['Address'] ?? $attr['Place_addr'] ?? null; + $display = $attr['LongLabel'] ?? $c['address'] ?? $attr['Place_addr'] ?? $query; + $street = $attr['Address'] ?? $attr['Place_addr'] ?? null; return [ 'display_name' => $display, @@ -154,9 +231,65 @@ class Geocoder 'state' => $attr['Region'] ?? null, 'postal' => $attr['Postal'] ?? null, 'country' => $attr['CountryCode'] ?? null, - // location.x = lon, location.y = lat 'lat' => $loc['y'] ?? null, 'lon' => $loc['x'] ?? null, ]; } + + /** + * get top-n suggestions for a free-form query + */ + public function suggestions(string $query, int $limit = 5, ?User $user = null): array + { + $provider = $this->cfg["provider"] ?? "arcgis"; + + if (mb_strlen(trim($query)) < 3) { + return []; + } + + return match ($provider) { + "arcgis" => $this->arcgisSuggestions($query, $limit), + default => [], + }; + } + + /** + * get the suggestions from arcgis + */ + private function arcgisSuggestions(string $query, int $limit): array + { + $params = [ + "singleLine" => $query, + "outFields" => $this->cfg["arcgis"]["out_fields"] ?? "*", + "maxLocations" => $limit, + // you can bias results with 'countryCode' or 'location' here if desired + "f" => "pjson", + "token" => $this->cfg["arcgis"]["api_key"], + ]; + + if (!empty($this->cfg["arcgis"]["store"])) { + $params["forStorage"] = "true"; + } + + $res = $this->http()->get( + $this->arcgisBase() . "/findAddressCandidates", + $params, + ); + if (!$res->ok()) { + return []; + } + + $json = $res->json(); + $cands = array_slice($json["candidates"] ?? [], 0, $limit); + + // normalize to the same structure used elsewhere + return array_values( + array_filter( + array_map( + fn($c) => $this->normalizeArcgisCandidate($c, $query), + $cands, + ), + ), + ); + } } diff --git a/config/services.php b/config/services.php index 89664da..3406d08 100644 --- a/config/services.php +++ b/config/services.php @@ -1,7 +1,6 @@ [ - 'token' => env('POSTMARK_TOKEN'), + "postmark" => [ + "token" => env("POSTMARK_TOKEN"), ], - 'resend' => [ - 'key' => env('RESEND_KEY'), + "resend" => [ + "key" => env("RESEND_KEY"), ], - 'ses' => [ - 'key' => env('AWS_ACCESS_KEY_ID'), - 'secret' => env('AWS_SECRET_ACCESS_KEY'), - 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), + "ses" => [ + "key" => env("AWS_ACCESS_KEY_ID"), + "secret" => env("AWS_SECRET_ACCESS_KEY"), + "region" => env("AWS_DEFAULT_REGION", "us-east-1"), ], - 'slack' => [ - 'notifications' => [ - 'bot_user_oauth_token' => env('SLACK_BOT_USER_OAUTH_TOKEN'), - 'channel' => env('SLACK_BOT_USER_DEFAULT_CHANNEL'), + "slack" => [ + "notifications" => [ + "bot_user_oauth_token" => env("SLACK_BOT_USER_OAUTH_TOKEN"), + "channel" => env("SLACK_BOT_USER_DEFAULT_CHANNEL"), ], ], /* custom geocoding service values */ - 'geocoding' => [ - 'provider' => env('GEOCODER', 'arcgis'), - 'timeout' => (int) env('GEOCODER_TIMEOUT', 20), - 'user_agent' => env('GEOCODER_USER_AGENT', 'Kithkin/LocalDev'), - 'arcgis' => [ - 'api_key' => env('ARCGIS_API_KEY'), - 'store' => (bool) env('ARCGIS_STORE_RESULTS', true), - 'endpoint' => 'https://geocode-api.arcgis.com/arcgis/rest/services/World/GeocodeServer', - // keep these compact and stable - 'out_fields' => 'Match_addr,Addr_type,PlaceName,Place_addr,Address,City,Region,Postal,CountryCode,LongLabel', - 'max_results' => 1, + "geocoding" => [ + "provider" => env("GEOCODER", "arcgis"), + "timeout" => (int) env("GEOCODER_TIMEOUT", 20), + "user_agent" => env("GEOCODER_USER_AGENT", "Kithkin/LocalDev"), + "arcgis" => [ + "api_key" => env("ARCGIS_API_KEY"), + "store" => (bool) env("ARCGIS_STORE_RESULTS", true), + "endpoint" => + "https://geocode-api.arcgis.com/arcgis/rest/services/World/GeocodeServer", + "country_code" => env("GEOCODER_COUNTRY", "USA"), + "categories" => env("GEOCODER_CATEGORIES", "POI,Address"), + "out_fields" => + "Match_addr,Addr_type,PlaceName,Place_addr,Address,City,Region,Postal,CountryCode,LongLabel", + "max_results" => 1, ], ], - ]; diff --git a/database/migrations/2025_08_20_000001_add_place_name_to_locations.php b/database/migrations/2025_08_20_000001_add_place_name_to_locations.php new file mode 100644 index 0000000..697349d --- /dev/null +++ b/database/migrations/2025_08_20_000001_add_place_name_to_locations.php @@ -0,0 +1,23 @@ +string('place_name', 255)->nullable()->after('display_name'); + $t->index('place_name', 'locations_place_name_idx'); + }); + } + + public function down(): void + { + Schema::table('locations', function (Blueprint $t) { + $t->dropIndex('locations_place_name_idx'); + $t->dropColumn('place_name'); + }); + } +}; diff --git a/database/migrations/2025_08_21_000000_user_address_improvements.php b/database/migrations/2025_08_21_000000_user_address_improvements.php new file mode 100644 index 0000000..63b1b60 --- /dev/null +++ b/database/migrations/2025_08_21_000000_user_address_improvements.php @@ -0,0 +1,79 @@ +bigIncrements('id'); + + // your users.id is char(26) + $table->char('user_id', 26); + $table->string('label', 100)->nullable(); // "Home", "Office" + $table->string('kind', 20)->nullable(); // "billing", "shipping", "home", "work" + + // raw, user-entered fields + $table->string('line1', 255)->nullable(); + $table->string('line2', 255)->nullable(); + $table->string('city', 100)->nullable(); + $table->string('state', 64)->nullable(); + $table->string('postal', 32)->nullable(); + $table->string('country', 64)->nullable(); + $table->string('phone', 32)->nullable(); + + // optional normalized location + $table->unsignedBigInteger('location_id')->nullable(); + + // nullable so unique index allows many NULLs (only 1 with value 1) + $table->boolean('is_primary')->nullable()->default(null); + + $table->timestamps(); + + // helpful indexes + $table->index(['user_id', 'kind']); + $table->index('postal'); + $table->foreign('user_id')->references('id')->on('users')->cascadeOnDelete(); + $table->foreign('location_id')->references('id')->on('locations')->nullOnDelete(); + + // enforce at most one primary per (user,kind) + $table->unique(['user_id', 'kind', 'is_primary'], 'user_kind_primary_unique'); + }); + + /** + * 2) lightweight geocoder bias on users (postal/country + optional centroid). + */ + Schema::table('users', function (Blueprint $table) { + $table->string('postal', 32)->nullable()->after('timezone'); + $table->string('country', 64)->nullable()->after('postal'); + $table->decimal('lat', 9, 6)->nullable()->after('country'); + $table->decimal('lon', 9, 6)->nullable()->after('lat'); + + $table->index('postal'); + }); + } + + public function down(): void + { + Schema::table('users', function (Blueprint $table) { + if (Schema::hasColumn('users', 'postal')) $table->dropIndex(['postal']); + foreach (['postal','country','lat','lon'] as $col) { + if (Schema::hasColumn('users', $col)) $table->dropColumn($col); + } + }); + + Schema::table('user_addresses', function (Blueprint $table) { + if (Schema::hasColumn('user_addresses', 'user_id')) $table->dropForeign(['user_id']); + if (Schema::hasColumn('user_addresses', 'location_id')) $table->dropForeign(['location_id']); + }); + + Schema::dropIfExists('user_addresses'); + } +}; \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 7173455..20f27b2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4,12 +4,12 @@ "requires": true, "packages": { "": { - "name": "kithkin", "devDependencies": { "@tailwindcss/forms": "^0.5.2", "@tailwindcss/postcss": "^4.1.11", "@tailwindcss/vite": "^4.1.11", "axios": "^1.8.2", + "blade-formatter": "^1.44.2", "concurrently": "^9.0.1", "htmx.org": "^2.0.6", "laravel-vite-plugin": "^1.2.0", @@ -45,6 +45,19 @@ "node": ">=6.0.0" } }, + "node_modules/@babel/runtime-corejs3": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.28.3.tgz", + "integrity": "sha512-LKYxD2CIfocUFNREQ1yk+dW+8OH8CRqmgatBZYXb+XhuObO8wsDpEoCNri5bKld9cnj8xukqZjxSX8p1YiRF8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "core-js-pure": "^3.43.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.25.6", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.6.tgz", @@ -487,6 +500,24 @@ "node": ">=18" } }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/@isaacs/fs-minipass": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", @@ -539,6 +570,76 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@one-ini/wasm": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@one-ini/wasm/-/wasm-0.1.1.tgz", + "integrity": "sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@prettier/plugin-php": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@prettier/plugin-php/-/plugin-php-0.24.0.tgz", + "integrity": "sha512-x9l65fCE/pgoET6RQowgdgG8Xmzs44z6j6Hhg3coINCyCw9JBGJ5ZzMR2XHAM2jmAdbJAIgqB6cUn4/3W3XLTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "linguist-languages": "^8.0.0", + "php-parser": "^3.2.5" + }, + "peerDependencies": { + "prettier": "^3.0.0" + } + }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.45.1", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.45.1.tgz", @@ -819,6 +920,69 @@ "win32" ] }, + "node_modules/@shufo/tailwindcss-class-sorter": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@shufo/tailwindcss-class-sorter/-/tailwindcss-class-sorter-3.0.1.tgz", + "integrity": "sha512-y9SMobvwElX2G6vdg4odJ6UL6hu/o5RlMsdwEeDLGaqHU3BLSw9CeitGgBus6kadjjDdT2wseG0Tl5yXWdc4UQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "escalade": "^3.1.1", + "object-hash": "^3.0.0", + "tailwindcss": "^3.3.2" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@shufo/tailwindcss-class-sorter/node_modules/jiti": { + "version": "1.21.7", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", + "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/@shufo/tailwindcss-class-sorter/node_modules/tailwindcss": { + "version": "3.4.17", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz", + "integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==", + "dev": true, + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.6.0", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.2", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.21.6", + "lilconfig": "^3.1.3", + "micromatch": "^4.0.8", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.1.1", + "postcss": "^8.4.47", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.2", + "postcss-nested": "^6.2.0", + "postcss-selector-parser": "^6.1.2", + "resolve": "^1.22.8", + "sucrase": "^3.35.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/@tailwindcss/forms": { "version": "0.5.10", "resolved": "https://registry.npmjs.org/@tailwindcss/forms/-/forms-0.5.10.tgz", @@ -1130,6 +1294,63 @@ "dev": true, "license": "MIT" }, + "node_modules/abbrev": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-2.0.0.tgz", + "integrity": "sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/aigle": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/aigle/-/aigle-1.14.1.tgz", + "integrity": "sha512-bCmQ65CEebspmpbWFs6ab3S27TNyVH1b5MledX8KoiGxUhsJmPUUGpaoSijhwawNnq5Lt8jbcq7Z7gUAD0nuTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "aigle-core": "^1.0.0" + } + }, + "node_modules/aigle-core": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/aigle-core/-/aigle-core-1.0.0.tgz", + "integrity": "sha512-uGFWPumk5DLvYnUphNnff+kWC8VeAnjPbbU8ovsSHflKXGX77SD7cAN/aSBCLX3xnoJAM9KdtRgxUygRnSSu7A==", + "dev": true, + "license": "MIT" + }, + "node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.0.tgz", + "integrity": "sha512-TKY5pyBkHyADOPYlRT9Lx6F544mPl0vS5Ew7BJ45hA08Q+t3GjbueLliBWN3sMICk6+y7HdyxSzC4bWS8baBdg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, "node_modules/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -1146,6 +1367,34 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true, + "license": "MIT" + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true, + "license": "MIT" + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -1165,6 +1414,140 @@ "proxy-from-env": "^1.1.0" } }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/blade-formatter": { + "version": "1.44.2", + "resolved": "https://registry.npmjs.org/blade-formatter/-/blade-formatter-1.44.2.tgz", + "integrity": "sha512-ulULHtiDVfaaFq80tGFtueK7c85lrkAyV1WK+Z8ubKJzOaaQlPOhYOaQFkC7JTsZZIjkkYFXWKGA4SZAmKT5mg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@prettier/plugin-php": "^0.24.0", + "@shufo/tailwindcss-class-sorter": "3.0.1", + "aigle": "^1.14.1", + "ajv": "^8.9.0", + "chalk": "^4.1.0", + "concat-stream": "^2.0.0", + "detect-indent": "^6.0.0", + "find-config": "^1.0.0", + "glob": "^10.0.0", + "html-attribute-sorter": "^0.4.3", + "ignore": "^6.0.0", + "js-beautify": "^1.15.4", + "lodash": "^4.17.19", + "php-parser": "3.2.5", + "prettier": "^3.2.5", + "string-replace-async": "^2.0.0", + "tailwindcss": "^3.1.8", + "vscode-oniguruma": "1.7.0", + "vscode-textmate": "^7.0.1", + "xregexp": "^5.0.1", + "yargs": "^17.3.1" + }, + "bin": { + "blade-formatter": "bin/blade-formatter.cjs" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/blade-formatter/node_modules/jiti": { + "version": "1.21.7", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", + "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/blade-formatter/node_modules/tailwindcss": { + "version": "3.4.17", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz", + "integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==", + "dev": true, + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.6.0", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.2", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.21.6", + "lilconfig": "^3.1.3", + "micromatch": "^4.0.8", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.1.1", + "postcss": "^8.4.47", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.2", + "postcss-nested": "^6.2.0", + "postcss-selector-parser": "^6.1.2", + "resolve": "^1.22.8", + "sucrase": "^3.35.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, "node_modules/call-bind-apply-helpers": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", @@ -1179,6 +1562,16 @@ "node": ">= 0.4" } }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -1209,6 +1602,44 @@ "node": ">=8" } }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/chownr": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", @@ -1330,6 +1761,32 @@ "node": ">= 0.8" } }, + "node_modules/commander": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/concat-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", + "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", + "dev": true, + "engines": [ + "node >= 6.0" + ], + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.0.2", + "typedarray": "^0.0.6" + } + }, "node_modules/concurrently": { "version": "9.2.0", "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.2.0.tgz", @@ -1356,6 +1813,57 @@ "url": "https://github.com/open-cli-tools/concurrently?sponsor=1" } }, + "node_modules/config-chain": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz", + "integrity": "sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ini": "^1.3.4", + "proto-list": "~1.2.1" + } + }, + "node_modules/core-js-pure": { + "version": "3.45.0", + "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.45.0.tgz", + "integrity": "sha512-OtwjqcDpY2X/eIIg1ol/n0y/X8A9foliaNt1dSK0gV3J2/zw+89FcNG3mPK+N8YWts4ZFUPxnrAzsxs/lf8yDA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -1366,6 +1874,16 @@ "node": ">=0.4.0" } }, + "node_modules/detect-indent": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-6.1.0.tgz", + "integrity": "sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/detect-libc": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", @@ -1376,6 +1894,20 @@ "node": ">=8" } }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true, + "license": "MIT" + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -1391,6 +1923,55 @@ "node": ">= 0.4" } }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/editorconfig": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/editorconfig/-/editorconfig-1.0.4.tgz", + "integrity": "sha512-L9Qe08KWTlqYMVvMcTIvMAdl1cDUubzRNYL+WfA4bLDMHe4nemKkpmYzkznE1FwLKu0EEmy6obgQKzMJrg4x9Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@one-ini/wasm": "0.1.1", + "commander": "^10.0.0", + "minimatch": "9.0.1", + "semver": "^7.5.3" + }, + "bin": { + "editorconfig": "bin/editorconfig" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/editorconfig/node_modules/minimatch": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.1.tgz", + "integrity": "sha512-0jWhJpD/MdhPXwPuiRkCbfYfSKp2qnn2eOc279qI7f+osl/l+prKSrvhg157zSYvx/1nmgn2NqdT6k2Z7zSH9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, "node_modules/enhanced-resolve": { "version": "5.18.2", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.2.tgz", @@ -1506,6 +2087,96 @@ "node": ">=6" } }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-uri": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.6.tgz", + "integrity": "sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-config": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/find-config/-/find-config-1.0.0.tgz", + "integrity": "sha512-Z+suHH+7LSE40WfUeZPIxSxypCWvrzdVc60xAjUShZeT5eMWM0/FQUduq3HjluyfAHWvC/aOBkT1pTZktyF/jg==", + "dev": true, + "license": "MIT", + "dependencies": { + "user-home": "^2.0.0" + }, + "engines": { + "node": ">= 0.12" + } + }, "node_modules/follow-redirects": { "version": "1.15.9", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", @@ -1527,6 +2198,23 @@ } } }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/form-data": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", @@ -1618,6 +2306,40 @@ "node": ">= 0.4" } }, + "node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -1690,6 +2412,16 @@ "node": ">= 0.4" } }, + "node_modules/html-attribute-sorter": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/html-attribute-sorter/-/html-attribute-sorter-0.4.3.tgz", + "integrity": "sha512-HWSvaXJki44tg0uR1t+j5pRdUVpNiZcJaoB/PFhss/YoAw9cxUDLCpIBbLWQmKjBQfWk91P6LaRnredEyabrDw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12.0.0" + } + }, "node_modules/htmx.org": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/htmx.org/-/htmx.org-2.0.6.tgz", @@ -1697,6 +2429,69 @@ "dev": true, "license": "0BSD" }, + "node_modules/ignore": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-6.0.2.tgz", + "integrity": "sha512-InwqeHHN2XpumIkMvpl/DCJVrAHgCsG5+cn1XlnLWGwtZBm8QJfSusItfrwx81CTp5agNZqpKU2J/ccC5nGT4A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true, + "license": "ISC" + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", @@ -1707,6 +2502,52 @@ "node": ">=8" } }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, "node_modules/jiti": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz", @@ -1717,6 +2558,45 @@ "jiti": "lib/jiti-cli.mjs" } }, + "node_modules/js-beautify": { + "version": "1.15.4", + "resolved": "https://registry.npmjs.org/js-beautify/-/js-beautify-1.15.4.tgz", + "integrity": "sha512-9/KXeZUKKJwqCXUdBxFJ3vPh467OCckSBmYDwSK/EtV090K+iMJ7zx2S3HLVDIWFQdqMIsZWbnaGiba18aWhaA==", + "dev": true, + "license": "MIT", + "dependencies": { + "config-chain": "^1.1.13", + "editorconfig": "^1.0.4", + "glob": "^10.4.2", + "js-cookie": "^3.0.5", + "nopt": "^7.2.1" + }, + "bin": { + "css-beautify": "js/bin/css-beautify.js", + "html-beautify": "js/bin/html-beautify.js", + "js-beautify": "js/bin/js-beautify.js" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/js-cookie": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz", + "integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, "node_modules/laravel-vite-plugin": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/laravel-vite-plugin/-/laravel-vite-plugin-1.3.0.tgz", @@ -1976,6 +2856,33 @@ "url": "https://opencollective.com/parcel" } }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/linguist-languages": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/linguist-languages/-/linguist-languages-8.1.0.tgz", + "integrity": "sha512-mg7zk9Lz89MJYvie41yUTmgeMwhN53gKQigNU1i+1JAGNqGRvF59RLhhkKCRYfZLt3rVgWcVSRrj0fxug5R98A==", + "dev": true, + "license": "MIT" + }, "node_modules/lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", @@ -1983,6 +2890,13 @@ "dev": true, "license": "MIT" }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, "node_modules/magic-string": { "version": "0.30.17", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", @@ -2003,6 +2917,30 @@ "node": ">= 0.4" } }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, "node_modules/mime-db": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", @@ -2036,6 +2974,22 @@ "mini-svg-data-uri": "cli.js" } }, + "node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/minipass": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", @@ -2075,6 +3029,18 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, "node_modules/nanoid": { "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", @@ -2094,6 +3060,110 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/nopt": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-7.2.1.tgz", + "integrity": "sha512-taM24ViiimT/XntxbPyJQzCG+p4EKOpgD3mxFwW38mGjVUrfERQOeY4EDHjdnptttfHuHQXFx+lTP08Q+mLa/w==", + "dev": true, + "license": "ISC", + "dependencies": { + "abbrev": "^2.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/os-homedir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", + "integrity": "sha512-B5JU3cabzk8c67mRRd3ECmROafjYMXbuzlwtqdM8IbS8ktlTix8aFGb2bAGKrSRIlnfKwovGUUr72JUPyOb6kQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/php-parser": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/php-parser/-/php-parser-3.2.5.tgz", + "integrity": "sha512-M1ZYlALFFnESbSdmRtTQrBFUHSriHgPhgqtTF/LCbZM4h7swR5PHtUceB2Kzby5CfqcsYwBn7OXTJ0+8Sajwkw==", + "dev": true, + "license": "BSD-3-Clause" + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -2114,6 +3184,26 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, "node_modules/postcss": { "version": "8.5.6", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", @@ -2143,6 +3233,150 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz", + "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz", + "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.0.0", + "yaml": "^2.3.4" + }, + "engines": { + "node": ">= 14" + }, + "peerDependencies": { + "postcss": ">=8.0.9", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "postcss": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/postcss-nested": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", + "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.1.1" + }, + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/prettier": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", + "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/proto-list": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", + "integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==", + "dev": true, + "license": "ISC" + }, "node_modules/proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", @@ -2150,6 +3384,65 @@ "dev": true, "license": "MIT" }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -2160,6 +3453,48 @@ "node": ">=0.10.0" } }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, "node_modules/rollup": { "version": "4.45.1", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.45.1.tgz", @@ -2200,6 +3535,30 @@ "fsevents": "~2.3.2" } }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, "node_modules/rxjs": { "version": "7.8.2", "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", @@ -2210,6 +3569,63 @@ "tslib": "^2.1.0" } }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/shell-quote": { "version": "1.8.3", "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz", @@ -2223,6 +3639,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -2233,6 +3662,163 @@ "node": ">=0.10.0" } }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-replace-async": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/string-replace-async/-/string-replace-async-2.0.0.tgz", + "integrity": "sha512-AHMupZscUiDh07F1QziX7PLoB1DQ/pzu19vc8Xa8LwZcgnOXaw7yCgBuSYrxVEfaM2d8scc3Gtp+i+QJZV+spw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12" + } + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/sucrase": { + "version": "3.35.0", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", + "integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "glob": "^10.3.10", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/sucrase/node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, "node_modules/supports-color": { "version": "8.1.1", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", @@ -2249,6 +3835,19 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/tailwindcss": { "version": "4.1.11", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.11.tgz", @@ -2284,6 +3883,29 @@ "node": ">=18" } }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/tinyglobby": { "version": "0.2.14", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz", @@ -2329,6 +3951,19 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, "node_modules/tree-kill": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", @@ -2339,6 +3974,13 @@ "tree-kill": "cli.js" } }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", @@ -2346,6 +3988,33 @@ "dev": true, "license": "0BSD" }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/user-home": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/user-home/-/user-home-2.0.0.tgz", + "integrity": "sha512-KMWqdlOcjCYdtIJpicDSFBQ8nFwS2i9sslAd6f4+CBGcU4gist2REnr2fxj2YocvJFxSF3ZOHLYLVZnUxv4BZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "os-homedir": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, "node_modules/vite": { "version": "6.3.5", "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz", @@ -2460,6 +4129,141 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/vscode-oniguruma": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/vscode-oniguruma/-/vscode-oniguruma-1.7.0.tgz", + "integrity": "sha512-L9WMGRfrjOhgHSdOYgCt/yRMsXzLDJSL7BPrOZt73gU0iWO4mpqzqQzOz5srxqTvMBaR0XZTSrVWo4j55Rc6cA==", + "dev": true, + "license": "MIT" + }, + "node_modules/vscode-textmate": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/vscode-textmate/-/vscode-textmate-7.0.4.tgz", + "integrity": "sha512-9hJp0xL7HW1Q5OgGe03NACo7yiCTMEk3WU/rtKXUbncLtdg6rVVNJnHwD88UhbIYU2KoxY0Dih0x+kIsmUKn2A==", + "dev": true, + "license": "MIT" + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/xregexp": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/xregexp/-/xregexp-5.1.2.tgz", + "integrity": "sha512-6hGgEMCGhqCTFEJbqmWrNIPqfpdirdGWkqshu7fFZddmTSfgv5Sn9D2SaKloR79s5VUiUlpwzg3CM3G6D3VIlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime-corejs3": "^7.26.9" + } + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", @@ -2486,8 +4290,6 @@ "integrity": "sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==", "dev": true, "license": "ISC", - "optional": true, - "peer": true, "bin": { "yaml": "bin.mjs" }, diff --git a/package.json b/package.json index 388daaa..3977b44 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "@tailwindcss/postcss": "^4.1.11", "@tailwindcss/vite": "^4.1.11", "axios": "^1.8.2", + "blade-formatter": "^1.44.2", "concurrently": "^9.0.1", "htmx.org": "^2.0.6", "laravel-vite-plugin": "^1.2.0", diff --git a/resources/css/etc/layout.css b/resources/css/etc/layout.css index 06d4e8a..bd9edd0 100644 --- a/resources/css/etc/layout.css +++ b/resources/css/etc/layout.css @@ -35,7 +35,7 @@ body { /* bottom items */ .bottom { - @apply pb-6 2xl:pb-8; + @apply flex flex-col items-center pb-6 2xl:pb-8; } /* app buttons */ diff --git a/resources/js/app.js b/resources/js/app.js index a194545..ba48ada 100644 --- a/resources/js/app.js +++ b/resources/js/app.js @@ -8,8 +8,14 @@ window.htmx = htmx; htmx.config.historyEnabled = true; // HX-Boost back/forward support htmx.logger = console.log; // verbose logging during dev +// csrf on htmx requests +document.addEventListener('htmx:configRequest', (evt) => { + const token = document.querySelector('meta[name="csrf-token"]')?.content + if (token) evt.detail.headers['X-CSRF-TOKEN'] = token +}) + // calendar toggle -// * progressive enhancement on html form with no js +// > progressive enhancement on html form with no js document.addEventListener('change', event => { const checkbox = event.target; diff --git a/resources/views/calendar/settings/index.blade.php b/resources/views/calendar/settings/index.blade.php index b464dda..7ed086d 100644 --- a/resources/views/calendar/settings/index.blade.php +++ b/resources/views/calendar/settings/index.blade.php @@ -2,7 +2,7 @@

- {{ __('Settings') }} + {{ __('Calendar') }}

diff --git a/resources/views/calendar/show.blade.php b/resources/views/calendar/show.blade.php index ac47bbc..1e4d302 100644 --- a/resources/views/calendar/show.blade.php +++ b/resources/views/calendar/show.blade.php @@ -6,12 +6,12 @@
- {{ __('Edit') }} - {{ __('Add Event') }} @@ -90,7 +90,7 @@ @endif
- {{ __('Edit') }} + {{ __('Edit') }} @empty diff --git a/resources/views/components/button/icon.blade.php b/resources/views/components/button/icon.blade.php index 7285354..0b1c0da 100644 --- a/resources/views/components/button/icon.blade.php +++ b/resources/views/components/button/icon.blade.php @@ -3,7 +3,8 @@ 'size' => 'default', 'type' => 'button', 'class' => '', - 'label' => 'Icon button' ]) + 'label' => 'Icon button', + 'href' => null ]) @php $variantClass = match ($variant) { @@ -17,8 +18,22 @@ $sizeClass = match ($size) { 'lg' => 'button--lg', default => '', }; + +$element = match ($type) { + 'anchor' => 'a', + 'button' => 'button', + 'submit' => 'button', + default => 'button', +}; + +$type = match ($type) { + 'anchor' => '', + 'button' => 'type="button"', + 'submit' => 'type="submit"', + default => '', +} @endphp - + diff --git a/resources/views/components/button/index.blade.php b/resources/views/components/button/index.blade.php index 907cbec..1bb4779 100644 --- a/resources/views/components/button/index.blade.php +++ b/resources/views/components/button/index.blade.php @@ -3,7 +3,8 @@ 'size' => 'default', 'type' => 'button', 'class' => '', - 'label' => 'Icon button' ]) + 'label' => '', + 'href' => null ]) @php $variantClass = match ($variant) { @@ -17,8 +18,22 @@ $sizeClass = match ($size) { 'lg' => 'button--lg', default => '', }; + +$element = match ($type) { + 'anchor' => 'a', + 'button' => 'button type="button"', + 'submit' => 'button type="submit"', + default => 'button', +}; + +$type = match ($type) { + 'anchor' => '', + 'button' => 'type="button"', + 'submit' => 'type="submit"', + default => '', +} @endphp - + diff --git a/resources/views/components/calendar/day.blade.php b/resources/views/components/calendar/day.blade.php index 2bd9df2..a6efa09 100644 --- a/resources/views/components/calendar/day.blade.php +++ b/resources/views/components/calendar/day.blade.php @@ -21,8 +21,8 @@ $color = $event['color'] ?? '#999'; @endphp +
+ {{ __('General settings') }} + +
  • + + + Language and region + +
  • +
    +
    +
    + {{ __('Add a calendar') }} + +
  • + + + Subscribe to a calendar + +
  • +
    +
    + diff --git a/resources/views/event/form.blade.php b/resources/views/event/form.blade.php index 36bdafc..cb3e8f2 100644 --- a/resources/views/event/form.blade.php +++ b/resources/views/event/form.blade.php @@ -6,7 +6,7 @@ {{-- “Back” breadcrumb --}} -
    ← {{ $calendar->name }} @@ -18,8 +18,8 @@
    + ? route('calendar.event.update', [$calendar, $event]) + : route('calendar.event.store', $calendar) }}"> @csrf @if($event->exists) @@ -45,8 +45,31 @@ {{-- Location --}}
    - + + + {{-- suggestion dropdown target --}} +
    + + {{-- hidden fields (filled when user clicks a suggestion; handy for step #2) --}} + + + + + + + + + +
    @@ -84,7 +107,7 @@ {{-- Submit --}}
    - {{ __('Cancel') }} diff --git a/resources/views/event/partials/details.blade.php b/resources/views/event/partials/details.blade.php index edce782..802884a 100644 --- a/resources/views/event/partials/details.blade.php +++ b/resources/views/event/partials/details.blade.php @@ -14,7 +14,7 @@

    @if ($event->meta->location) -

    Where: {{ $event->meta->location }}

    +

    Where: {{ $event->meta->location_label }}

    @endif @if ($event->meta->description) diff --git a/resources/views/event/partials/suggestions.blade.php b/resources/views/event/partials/suggestions.blade.php new file mode 100644 index 0000000..7fd26c4 --- /dev/null +++ b/resources/views/event/partials/suggestions.blade.php @@ -0,0 +1,39 @@ +@if (empty($suggestions)) +
    +@else +
      + @foreach ($suggestions as $s) +
    • + +
    • + @endforeach +
    +@endif diff --git a/resources/views/event/show.blade.php b/resources/views/event/show.blade.php index 77517ab..50dbf40 100644 --- a/resources/views/event/show.blade.php +++ b/resources/views/event/show.blade.php @@ -4,7 +4,7 @@ {{ $event->meta->title ?? '(no title)' }} - Edit diff --git a/resources/views/layouts/navigation.blade.php b/resources/views/layouts/navigation.blade.php index 9bd037e..6a95cb2 100644 --- a/resources/views/layouts/navigation.blade.php +++ b/resources/views/layouts/navigation.blade.php @@ -22,34 +22,12 @@
    - + - - - - - - - - -
    {{ Auth::user()->name }}
    - - {{ __('Profile') }} - - - - - @csrf - - - {{ __('Log Out') }} - - -
    -
    + + +
    diff --git a/resources/views/profile/edit.blade.php b/resources/views/profile/edit.blade.php index 57d38a9..134e9aa 100644 --- a/resources/views/profile/edit.blade.php +++ b/resources/views/profile/edit.blade.php @@ -13,6 +13,15 @@
    +
    +
    + @include('profile.partials.addresses-form', [ + 'home' => $home ?? null, + 'billing' => $billing ?? null, + ]) +
    +
    +
    @include('profile.partials.update-password-form') diff --git a/resources/views/profile/index.blade.php b/resources/views/profile/index.blade.php new file mode 100644 index 0000000..7930312 --- /dev/null +++ b/resources/views/profile/index.blade.php @@ -0,0 +1,30 @@ + + + +

    + {{ __('Profile') }} +

    + +
    + + +

    + @isset($data['title']) + {{ $data['title'] }} + @else + {{ __('Settings') }} + @endisset +

    +
    + + +
    + @isset($view) + @include($view, $data ?? []) + @else +

    {{ __('Pick an option in the sidebar…') }}

    + @endisset +
    + + + diff --git a/resources/views/profile/partials/addresses-form.blade.php b/resources/views/profile/partials/addresses-form.blade.php new file mode 100644 index 0000000..8220895 --- /dev/null +++ b/resources/views/profile/partials/addresses-form.blade.php @@ -0,0 +1,153 @@ +
    +
    +

    + {{ __('Addresses') }} +

    +

    + {{ __('Manage your Home and Billing addresses.') }} +

    +
    + +
    + @csrf + @method('patch') + + {{-- home address --}} +
    +

    {{ __('Home Address') }}

    + +
    + + + +
    + +
    + + + +
    + +
    + + + +
    + +
    +
    + + + +
    +
    + + + +
    +
    + + + +
    +
    + +
    +
    + + + +
    +
    + + + +
    +
    +
    + + {{-- billing address --}} +
    +

    {{ __('Billing Address') }}

    + +
    + + + +
    + +
    + + + +
    + +
    + + + +
    + +
    +
    + + + +
    +
    + + + +
    +
    + + + +
    +
    + +
    +
    + + + +
    +
    + + + +
    +
    +
    + +
    + {{ __('Save Addresses') }} + + @if (session('status') === 'addresses-updated') +

    {{ __('Saved.') }}

    + @endif +
    +
    +
    diff --git a/routes/web.php b/routes/web.php index f37cd15..76cea56 100644 --- a/routes/web.php +++ b/routes/web.php @@ -9,22 +9,22 @@ use App\Http\Controllers\CardController; use App\Http\Controllers\DavController; use App\Http\Controllers\EventController; use App\Http\Controllers\IcsController; +use App\Http\Controllers\LocationController; use App\Http\Controllers\SubscriptionController; use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken; -/* -|-------------------------------------------------------------------------- -| Public pages -|-------------------------------------------------------------------------- -*/ +/** + * + * public unauthenticated pages + */ Route::view('/', 'welcome'); -/* -|-------------------------------------------------------------------------- -| Breeze starter‑kit pages -|-------------------------------------------------------------------------- -*/ +/** + * + * starter pages + * @todo replace thse + */ Route::view('/dashboard', 'dashboard') ->middleware(['auth', 'verified']) @@ -34,11 +34,19 @@ Route::view('/settings', 'settings') ->middleware(['auth', 'verified']) ->name('settings'); -Route::middleware('auth')->group(function () { - /* User profile (generated by Breeze) */ - Route::get ('/profile', [ProfileController::class, 'edit' ])->name('profile.edit'); - Route::patch ('/profile', [ProfileController::class, 'update'])->name('profile.update'); - Route::delete('/profile', [ProfileController::class, 'destroy'])->name('profile.destroy'); +/** + * + * main authentication block + */ + +Route::middleware('auth')->group(function () +{ + /* user profile */ + Route::get('/profile/view', [ProfileController::class, 'index'])->name('profile.index'); + Route::get('/profile', [ProfileController::class, 'edit'])->name('profile.edit'); + Route::patch('/profile', [ProfileController::class, 'update'])->name('profile.update'); + Route::delete('/profile', [ProfileController::class, 'destroy'])->name('profile.destroy'); + Route::patch('/profile/addresses', [ProfileController::class, 'saveAddresses'])->name('profile.addresses.save'); /* calendar core */ Route::middleware('auth')->group(function () { @@ -48,7 +56,7 @@ Route::middleware('auth')->group(function () { }); /* calendar other */ - Route::middleware('auth') + Route::middleware(['web','auth']) ->prefix('calendar') ->name('calendar.') ->group(function () { @@ -63,22 +71,27 @@ Route::middleware('auth')->group(function () { // remote calendar subscriptions Route::resource('subscriptions', SubscriptionController::class) ->except(['show']); // index, create, store, edit, update, destroy + // events Route::prefix('{calendar}')->whereUuid('calendar')->group(function () { // create & store - Route::get ('event/create', [EventController::class, 'create'])->name('events.create'); - Route::post('event', [EventController::class, 'store' ])->name('events.store'); + Route::get ('event/create', [EventController::class, 'create'])->name('event.create'); + Route::post('event', [EventController::class, 'store' ])->name('event.store'); // read - Route::get ('event/{event}', [EventController::class, 'show' ])->name('events.show'); + Route::get ('event/{event}', [EventController::class, 'show' ])->name('event.show'); // edit & update - Route::get ('event/{event}/edit', [EventController::class, 'edit' ])->name('events.edit'); - Route::put ('event/{event}', [EventController::class, 'update'])->name('events.update'); + Route::get ('event/{event}/edit', [EventController::class, 'edit' ])->name('event.edit'); + Route::put ('event/{event}', [EventController::class, 'update'])->name('event.update'); // delete - Route::delete('event/{event}', [EventController::class, 'destroy'])->name('events.destroy'); + Route::delete('event/{event}', [EventController::class, 'destroy'])->name('event.destroy'); }); }); - /** address books */ + // autocomplete suggestions for event locations + Route::get('/location/suggest', [LocationController::class, 'suggest']) + ->name('location.suggest'); + + // address books Route::resource('book', BookController::class) ->names('book') // books.index, books.create, … ->parameter('book', 'book'); // {book} binding