Enhance website accessibility and improve user interface elements

Introduce ARIA attributes, improve focus management, and refine UI element styling for better accessibility and user experience across the application.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: c446bc9e-66df-438c-a86c-f53e6da13649
Replit-Commit-Checkpoint-Type: intermediate_checkpoint
This commit is contained in:
pac7
2025-09-23 22:25:16 +00:00
parent d978217577
commit 6391b3d81c
9 changed files with 189 additions and 107 deletions

View File

@@ -54,14 +54,14 @@ outputType = "webview"
localPort = 5000 localPort = 5000
externalPort = 80 externalPort = 80
[[ports]]
localPort = 33323
externalPort = 3002
[[ports]] [[ports]]
localPort = 41923 localPort = 41923
externalPort = 3000 externalPort = 3000
[[ports]]
localPort = 44757
externalPort = 3002
[[ports]] [[ports]]
localPort = 45245 localPort = 45245
externalPort = 3001 externalPort = 3001

View File

@@ -809,9 +809,6 @@
.min-h-20 { .min-h-20 {
min-height: calc(var(--spacing) * 20); min-height: calc(var(--spacing) * 20);
} }
.min-h-\[24px\] {
min-height: 24px;
}
.min-h-\[44px\] { .min-h-\[44px\] {
min-height: 44px; min-height: 44px;
} }
@@ -947,9 +944,6 @@
.min-w-16 { .min-w-16 {
min-width: calc(var(--spacing) * 16); min-width: calc(var(--spacing) * 16);
} }
.min-w-\[24px\] {
min-width: 24px;
}
.min-w-\[44px\] { .min-w-\[44px\] {
min-width: 44px; min-width: 44px;
} }
@@ -3091,6 +3085,11 @@
left: calc(var(--spacing) * 4); left: calc(var(--spacing) * 4);
} }
} }
.focus\:left-32 {
&:focus {
left: calc(var(--spacing) * 32);
}
}
.focus\:z-50 { .focus\:z-50 {
&:focus { &:focus {
z-index: 50; z-index: 50;
@@ -3162,6 +3161,11 @@
--tw-ring-color: var(--color-blue-500); --tw-ring-color: var(--color-blue-500);
} }
} }
.focus\:ring-gray-500 {
&:focus {
--tw-ring-color: var(--color-gray-500);
}
}
.focus\:ring-green-500 { .focus\:ring-green-500 {
&:focus { &:focus {
--tw-ring-color: var(--color-green-500); --tw-ring-color: var(--color-green-500);
@@ -3193,6 +3197,12 @@
--tw-ring-color: var(--color-yellow-500); --tw-ring-color: var(--color-yellow-500);
} }
} }
.focus\:ring-offset-1 {
&:focus {
--tw-ring-offset-width: 1px;
--tw-ring-offset-shadow: var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
}
}
.focus\:ring-offset-2 { .focus\:ring-offset-2 {
&:focus { &:focus {
--tw-ring-offset-width: 2px; --tw-ring-offset-width: 2px;
@@ -3215,6 +3225,11 @@
outline-style: none; outline-style: none;
} }
} }
.focus\:ring-inset {
&:focus {
--tw-ring-inset: inset;
}
}
.focus-visible\:ring-2 { .focus-visible\:ring-2 {
&:focus-visible { &:focus-visible {
--tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor); --tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor);
@@ -3371,6 +3386,11 @@
min-height: calc(var(--spacing) * 0); min-height: calc(var(--spacing) * 0);
} }
} }
.sm\:min-h-\[32px\] {
@media (width >= 40rem) {
min-height: 32px;
}
}
.sm\:min-h-\[400px\] { .sm\:min-h-\[400px\] {
@media (width >= 40rem) { @media (width >= 40rem) {
min-height: 400px; min-height: 400px;
@@ -3406,9 +3426,9 @@
width: auto; width: auto;
} }
} }
.sm\:min-w-0 { .sm\:min-w-\[32px\] {
@media (width >= 40rem) { @media (width >= 40rem) {
min-width: calc(var(--spacing) * 0); min-width: 32px;
} }
} }
.sm\:flex-1 { .sm\:flex-1 {

View File

@@ -90,16 +90,17 @@ Features:
@click.away="open = false"> @click.away="open = false">
<div class="relative"> <div class="relative">
<!-- Search Icon --> <!-- Search Icon with ARIA -->
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none"> <div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none" aria-hidden="true">
<svg class="h-5 w-5 text-gray-400 dark:text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="h-5 w-5 text-gray-400 dark:text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>
</svg> </svg>
</div> </div>
<!-- Search Input --> <!-- Search Input with Enhanced Accessibility -->
<input <input
x-ref="searchInput" x-ref="searchInput"
id="park-search"
type="text" type="text"
name="search" name="search"
x-model="search" x-model="search"
@@ -136,32 +137,40 @@ Features:
} }
" "
autocomplete="off" autocomplete="off"
role="combobox"
aria-expanded="false"
:aria-expanded="open"
aria-autocomplete="list"
aria-controls="search-suggestions"
aria-describedby="search-help-text search-live-region"
:aria-activedescendant="selectedIndex >= 0 ? `suggestion-${selectedIndex}` : null"
/> />
<!-- Loading Spinner --> <!-- Loading Spinner with ARIA -->
<div id="search-spinner" class="absolute inset-y-0 right-0 pr-3 flex items-center htmx-indicator"> <div id="search-spinner" class="absolute inset-y-0 right-0 pr-3 flex items-center htmx-indicator" aria-hidden="true">
<svg class="animate-spin h-5 w-5 text-gray-400" fill="none" viewBox="0 0 24 24"> <svg class="animate-spin h-5 w-5 text-gray-400" fill="none" viewBox="0 0 24 24" aria-hidden="true">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle> <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path> <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg> </svg>
</div> </div>
<!-- Clear Button --> <!-- Clear Button with Enhanced Accessibility -->
<button <button
x-show="search.length > 0" x-show="search.length > 0"
@click="clearSearch()" @click="clearSearch()"
type="button" type="button"
class="absolute inset-y-0 right-0 pr-3 flex items-center text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors htmx-indicator:hidden" class="absolute inset-y-0 right-0 pr-3 flex items-center text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors htmx-indicator:hidden min-w-[44px] min-h-[44px] justify-center"
aria-label="Clear search" aria-label="Clear search input"
title="Clear search" title="Clear search"
tabindex="0"
> >
<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg> </svg>
</button> </button>
</div> </div>
<!-- Autocomplete Dropdown --> <!-- Autocomplete Dropdown with ARIA -->
<div <div
x-show="open && suggestions.length > 0" x-show="open && suggestions.length > 0"
x-transition:enter="transition ease-out duration-100" x-transition:enter="transition ease-out duration-100"
@@ -172,15 +181,22 @@ Features:
x-transition:leave-end="transform opacity-0 scale-95" x-transition:leave-end="transform opacity-0 scale-95"
class="absolute z-50 w-full mt-1 bg-white border border-gray-200 rounded-lg shadow-lg dark:bg-gray-800 dark:border-gray-700 max-h-60 overflow-y-auto" class="absolute z-50 w-full mt-1 bg-white border border-gray-200 rounded-lg shadow-lg dark:bg-gray-800 dark:border-gray-700 max-h-60 overflow-y-auto"
style="display: none;" style="display: none;"
role="listbox"
aria-label="Search suggestions"
id="search-suggestions"
> >
<div class="py-1"> <div class="py-1">
<template x-for="(suggestion, index) in suggestions" :key="index"> <template x-for="(suggestion, index) in suggestions" :key="index">
<button <button
type="button" type="button"
class="w-full px-4 py-2 text-left text-sm text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700 flex items-center justify-between" class="w-full px-4 py-2 text-left text-sm text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700 flex items-center justify-between min-h-[44px] focus:outline-none focus:ring-2 focus:ring-blue-500"
:class="{ 'bg-gray-100 dark:bg-gray-700': selectedIndex === index }" :class="{ 'bg-gray-100 dark:bg-gray-700': selectedIndex === index }"
@click="selectSuggestion(suggestion)" @click="selectSuggestion(suggestion)"
@mouseenter="selectedIndex = index" @mouseenter="selectedIndex = index"
role="option"
:id="`suggestion-${index}`"
:aria-selected="selectedIndex === index"
:aria-label="`Select ${suggestion.name || suggestion}${suggestion.type ? ' - ' + suggestion.type : ''}`"
> >
<span x-text="suggestion.name || suggestion"></span> <span x-text="suggestion.name || suggestion"></span>
<template x-if="suggestion.type"> <template x-if="suggestion.type">
@@ -229,4 +245,19 @@ Features:
</div> </div>
{% endif %} {% endif %}
</div> </div>
<!-- Screen Reader Support Elements -->
<div id="search-help-text" class="sr-only">
Type to search parks. Use arrow keys to navigate suggestions, Enter to select, or Escape to close.
</div>
<!-- Live Region for Screen Reader Announcements -->
<div id="search-live-region"
aria-live="polite"
aria-atomic="true"
class="sr-only"
x-text="open && suggestions.length > 0 ?
`${suggestions.length} suggestion${suggestions.length !== 1 ? 's' : ''} available. Use arrow keys to navigate.` :
(search.length >= 2 && !loading && suggestions.length === 0 ? 'No suggestions found.' : '')">
</div>
</div> </div>

View File

@@ -28,10 +28,10 @@ Features:
/> />
{% if filters %} {% if filters %}
<div class="flex flex-wrap gap-2 {{ class }}"> <div class="flex flex-wrap gap-2 {{ class }}" role="group" aria-label="Active filters">
{% for filter_name, filter_value in filters.items %} {% for filter_name, filter_value in filters.items %}
{% if filter_value and filter_name != 'page' and filter_name != 'view_mode' %} {% if filter_value and filter_name != 'page' and filter_name != 'view_mode' %}
<div class="inline-flex items-center gap-2 px-3 py-1.5 sm:py-1 text-sm font-medium text-blue-700 dark:text-blue-300 bg-blue-50 dark:bg-blue-900/30 rounded-full border border-blue-200 dark:border-blue-700/50"> <div class="inline-flex items-center gap-2 px-3 py-1.5 sm:py-1 text-sm font-medium text-blue-700 dark:text-blue-300 bg-blue-50 dark:bg-blue-900/30 rounded-full border border-blue-200 dark:border-blue-700/50" role="group" aria-label="{{ filter_name|title }} filter: {{ filter_value }}">
<span class="capitalize text-xs sm:text-sm">{{ filter_name|title }}:</span> <span class="capitalize text-xs sm:text-sm">{{ filter_name|title }}:</span>
<span class="font-semibold text-xs sm:text-sm"> <span class="font-semibold text-xs sm:text-sm">
{% if filter_value == 'True' %} {% if filter_value == 'True' %}
@@ -44,15 +44,15 @@ Features:
</span> </span>
<button <button
type="button" type="button"
class="ml-1 p-1 sm:p-0.5 text-blue-600 dark:text-blue-300 hover:text-blue-800 dark:hover:text-blue-200 hover:bg-blue-100 dark:hover:bg-blue-800/50 rounded-full transition-all duration-200 min-w-[24px] min-h-[24px] sm:min-w-0 sm:min-h-0 flex items-center justify-center" class="ml-1 p-1 sm:p-0.5 text-blue-600 dark:text-blue-300 hover:text-blue-800 dark:hover:text-blue-200 hover:bg-blue-100 dark:hover:bg-blue-800/50 rounded-full transition-all duration-200 min-w-[44px] min-h-[44px] sm:min-w-[32px] sm:min-h-[32px] flex items-center justify-center focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-1"
hx-get="{% if base_url %}{{ base_url }}{% else %}{{ request.path }}{% endif %}?{% for name, value in request.GET.items %}{% if name != filter_name and name != 'page' %}{{ name }}={{ value }}&{% endif %}{% endfor %}" hx-get="{% if base_url %}{{ base_url }}{% else %}{{ request.path }}{% endif %}?{% for name, value in request.GET.items %}{% if name != filter_name and name != 'page' %}{{ name }}={{ value }}&{% endif %}{% endfor %}"
hx-target="#park-results" hx-target="#park-results"
hx-push-url="true" hx-push-url="true"
hx-indicator="#search-spinner" hx-indicator="#search-spinner"
aria-label="Remove {{ filter_name }} filter" aria-label="Remove {{ filter_name|title }} filter with value {{ filter_value }}"
title="Remove filter" title="Remove {{ filter_name|title }} filter"
> >
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg> </svg>
</button> </button>
@@ -63,15 +63,15 @@ Features:
{% if filters|length > 1 %} {% if filters|length > 1 %}
<button <button
type="button" type="button"
class="inline-flex items-center gap-1 px-3 py-1 text-sm font-medium text-gray-600 bg-gray-100 rounded-full border border-gray-200 hover:bg-gray-200 dark:bg-gray-700 dark:text-gray-300 dark:border-gray-600 dark:hover:bg-gray-600 transition-colors" class="inline-flex items-center gap-1 px-3 py-2 text-sm font-medium text-gray-600 bg-gray-100 rounded-full border border-gray-200 hover:bg-gray-200 dark:bg-gray-700 dark:text-gray-300 dark:border-gray-600 dark:hover:bg-gray-600 transition-colors min-h-[44px] focus:outline-none focus:ring-2 focus:ring-gray-500 focus:ring-offset-2"
hx-get="{% if base_url %}{{ base_url }}{% else %}{{ request.path }}{% endif %}" hx-get="{% if base_url %}{{ base_url }}{% else %}{{ request.path }}{% endif %}"
hx-target="#park-results" hx-target="#park-results"
hx-push-url="true" hx-push-url="true"
hx-indicator="#search-spinner" hx-indicator="#search-spinner"
aria-label="Clear all filters" aria-label="Clear all active filters"
title="Clear all filters" title="Clear all filters"
> >
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg> </svg>
Clear all Clear all

View File

@@ -48,8 +48,8 @@ Features:
{% if park %} {% if park %}
{% if view_mode == 'list' %} {% if view_mode == 'list' %}
{# Enhanced List View Item with CloudFlare Images #} {# Enhanced List View Item with CloudFlare Images and Accessibility #}
<article class="group bg-white/80 dark:bg-gray-800/80 backdrop-blur-sm border border-gray-200/50 dark:border-gray-700/50 rounded-xl shadow-lg hover:shadow-xl transition-all duration-300 transform hover:scale-[1.02] overflow-hidden {{ class }}"> <article class="group bg-white/80 dark:bg-gray-800/80 backdrop-blur-sm border border-gray-200/50 dark:border-gray-700/50 rounded-xl shadow-lg hover:shadow-xl transition-all duration-300 transform hover:scale-[1.02] overflow-hidden {{ class }}" role="article" aria-labelledby="park-title-{{ park.id }}" aria-describedby="park-description-{{ park.id }}">
<div class="p-4 sm:p-6"> <div class="p-4 sm:p-6">
<div class="flex flex-col sm:flex-row gap-4 sm:gap-6"> <div class="flex flex-col sm:flex-row gap-4 sm:gap-6">
{# Enhanced List View Image Section #} {# Enhanced List View Image Section #}
@@ -92,7 +92,7 @@ Features:
</div> </div>
{% endif %} {% endif %}
{# List View Status Badge Overlay #} {# List View Status Badge Overlay with Accessibility #}
<div class="absolute top-1.5 right-1.5 sm:top-2 sm:right-2"> <div class="absolute top-1.5 right-1.5 sm:top-2 sm:right-2">
<span class="inline-flex items-center px-1.5 py-0.5 sm:px-2 sm:py-1 rounded-full text-xs font-semibold border shrink-0 bg-white/95 backdrop-blur-sm shadow-sm <span class="inline-flex items-center px-1.5 py-0.5 sm:px-2 sm:py-1 rounded-full text-xs font-semibold border shrink-0 bg-white/95 backdrop-blur-sm shadow-sm
{% if park.status == 'operating' or park.status == 'OPERATING' %}text-green-700 border-green-200 {% if park.status == 'operating' or park.status == 'OPERATING' %}text-green-700 border-green-200
@@ -102,9 +102,12 @@ Features:
{% elif park.status == 'under_construction' or park.status == 'UNDER_CONSTRUCTION' %}text-blue-700 border-blue-200 {% elif park.status == 'under_construction' or park.status == 'UNDER_CONSTRUCTION' %}text-blue-700 border-blue-200
{% elif park.status == 'demolished' or park.status == 'DEMOLISHED' %}text-gray-700 border-gray-200 {% elif park.status == 'demolished' or park.status == 'DEMOLISHED' %}text-gray-700 border-gray-200
{% elif park.status == 'relocated' or park.status == 'RELOCATED' %}text-purple-700 border-purple-200 {% elif park.status == 'relocated' or park.status == 'RELOCATED' %}text-purple-700 border-purple-200
{% else %}text-gray-700 border-gray-200{% endif %}"> {% else %}text-gray-700 border-gray-200{% endif %}"
<span class="hidden sm:inline">{{ park.get_status_display }}</span> role="img"
<span class="sm:hidden">{{ park.get_status_display|truncatechars:3 }}</span> aria-label="Park status: {{ park.get_status_display }}"
title="Park status: {{ park.get_status_display }}">
<span class="hidden sm:inline" aria-hidden="true">{{ park.get_status_display }}</span>
<span class="sm:hidden" aria-hidden="true">{{ park.get_status_display|truncatechars:3 }}</span>
</span> </span>
</div> </div>
</div> </div>
@@ -113,9 +116,9 @@ Features:
{# Enhanced Main Content Section with Better Mobile Layout #} {# Enhanced Main Content Section with Better Mobile Layout #}
<div class="flex-1 min-w-0 flex flex-col justify-between"> <div class="flex-1 min-w-0 flex flex-col justify-between">
<div class="space-y-2 sm:space-y-3"> <div class="space-y-2 sm:space-y-3">
{# Enhanced Title with Better Mobile Typography #} {# Enhanced Title with Better Mobile Typography and Accessibility #}
<div class="flex items-start justify-between"> <div class="flex items-start justify-between">
<h2 class="text-lg sm:text-xl lg:text-2xl font-bold line-clamp-2 leading-tight"> <h3 id="park-title-{{ park.id }}" class="text-lg sm:text-xl lg:text-2xl font-bold line-clamp-2 leading-tight">
{% if park.slug %} {% if park.slug %}
<a href="{% url 'parks:park_detail' park.slug %}" <a href="{% url 'parks:park_detail' park.slug %}"
class="bg-gradient-to-r from-gray-900 to-gray-700 dark:from-white dark:to-gray-300 bg-clip-text text-transparent hover:from-blue-600 hover:to-purple-600 dark:hover:from-blue-400 dark:hover:to-purple-400 transition-all duration-300 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 rounded-sm" class="bg-gradient-to-r from-gray-900 to-gray-700 dark:from-white dark:to-gray-300 bg-clip-text text-transparent hover:from-blue-600 hover:to-purple-600 dark:hover:from-blue-400 dark:hover:to-purple-400 transition-all duration-300 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 rounded-sm"
@@ -127,7 +130,7 @@ Features:
{{ park.name }} {{ park.name }}
</span> </span>
{% endif %} {% endif %}
</h2> </h3>
{# View Details Arrow for Mobile #} {# View Details Arrow for Mobile #}
<div class="sm:hidden text-gray-400 group-hover:text-blue-600 dark:group-hover:text-blue-400 transition-colors duration-200 ml-2 flex-shrink-0"> <div class="sm:hidden text-gray-400 group-hover:text-blue-600 dark:group-hover:text-blue-400 transition-colors duration-200 ml-2 flex-shrink-0">
@@ -147,9 +150,9 @@ Features:
</div> </div>
{% endif %} {% endif %}
{# Enhanced Description #} {# Enhanced Description with Accessibility #}
{% if park.description %} {% if park.description %}
<p class="text-sm text-gray-600 dark:text-gray-400 line-clamp-2 leading-relaxed"> <p id="park-description-{{ park.id }}" class="text-sm text-gray-600 dark:text-gray-400 line-clamp-2 leading-relaxed">
{{ park.description|truncatewords:30 }} {{ park.description|truncatewords:30 }}
</p> </p>
{% endif %} {% endif %}
@@ -160,21 +163,27 @@ Features:
<div class="flex items-center justify-between pt-3 border-t border-gray-200/50 dark:border-gray-600/50 mt-3"> <div class="flex items-center justify-between pt-3 border-t border-gray-200/50 dark:border-gray-600/50 mt-3">
<div class="flex items-center space-x-3 sm:space-x-6 text-sm"> <div class="flex items-center space-x-3 sm:space-x-6 text-sm">
{% if park.ride_count %} {% if park.ride_count %}
<div class="flex items-center space-x-1.5 sm:space-x-2 px-2 sm:px-4 py-1.5 sm:py-2 bg-gradient-to-r from-blue-50 to-purple-50 dark:from-blue-900/20 dark:to-purple-900/20 rounded-lg border border-blue-200/50 dark:border-blue-800/50" title="{{ park.ride_count }} ride{{ park.ride_count|pluralize }}"> <div class="flex items-center space-x-1.5 sm:space-x-2 px-2 sm:px-4 py-1.5 sm:py-2 bg-gradient-to-r from-blue-50 to-purple-50 dark:from-blue-900/20 dark:to-purple-900/20 rounded-lg border border-blue-200/50 dark:border-blue-800/50"
<svg class="w-4 h-4 sm:w-5 sm:h-5 text-blue-600 dark:text-blue-400 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"> role="img"
aria-label="{{ park.ride_count }} ride{{ park.ride_count|pluralize }} available"
title="{{ park.ride_count }} ride{{ park.ride_count|pluralize }}">
<svg class="w-4 h-4 sm:w-5 sm:h-5 text-blue-600 dark:text-blue-400 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"/> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"/>
</svg> </svg>
<span class="font-semibold text-blue-700 dark:text-blue-300">{{ park.ride_count }}</span> <span class="font-semibold text-blue-700 dark:text-blue-300" aria-hidden="true">{{ park.ride_count }}</span>
<span class="text-blue-600 dark:text-blue-400 hidden sm:inline">rides</span> <span class="text-blue-600 dark:text-blue-400 hidden sm:inline" aria-hidden="true">rides</span>
</div> </div>
{% endif %} {% endif %}
{% if park.coaster_count %} {% if park.coaster_count %}
<div class="flex items-center space-x-1.5 sm:space-x-2 px-2 sm:px-4 py-1.5 sm:py-2 bg-gradient-to-r from-purple-50 to-pink-50 dark:from-purple-900/20 dark:to-pink-900/20 rounded-lg border border-purple-200/50 dark:border-purple-800/50" title="{{ park.coaster_count }} roller coaster{{ park.coaster_count|pluralize }}"> <div class="flex items-center space-x-1.5 sm:space-x-2 px-2 sm:px-4 py-1.5 sm:py-2 bg-gradient-to-r from-purple-50 to-pink-50 dark:from-purple-900/20 dark:to-pink-900/20 rounded-lg border border-purple-200/50 dark:border-purple-800/50"
<svg class="w-4 h-4 sm:w-5 sm:h-5 text-purple-600 dark:text-purple-400 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"> role="img"
aria-label="{{ park.coaster_count }} roller coaster{{ park.coaster_count|pluralize }} available"
title="{{ park.coaster_count }} roller coaster{{ park.coaster_count|pluralize }}">
<svg class="w-4 h-4 sm:w-5 sm:h-5 text-purple-600 dark:text-purple-400 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"/> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"/>
</svg> </svg>
<span class="font-semibold text-purple-700 dark:text-purple-300">{{ park.coaster_count }}</span> <span class="font-semibold text-purple-700 dark:text-purple-300" aria-hidden="true">{{ park.coaster_count }}</span>
<span class="text-purple-600 dark:text-purple-400 hidden sm:inline">coasters</span> <span class="text-purple-600 dark:text-purple-400 hidden sm:inline" aria-hidden="true">coasters</span>
</div> </div>
{% endif %} {% endif %}
</div> </div>
@@ -201,8 +210,8 @@ Features:
</div> </div>
</article> </article>
{% else %} {% else %}
{# Enhanced Grid View Item #} {# Enhanced Grid View Item with Accessibility #}
<article class="group bg-white/80 dark:bg-gray-800/80 backdrop-blur-sm border border-gray-200/50 dark:border-gray-700/50 rounded-xl shadow-lg hover:shadow-xl transition-all duration-300 transform hover:scale-105 hover:-rotate-1 overflow-hidden {{ class }}"> <article class="group bg-white/80 dark:bg-gray-800/80 backdrop-blur-sm border border-gray-200/50 dark:border-gray-700/50 rounded-xl shadow-lg hover:shadow-xl transition-all duration-300 transform hover:scale-105 hover:-rotate-1 overflow-hidden {{ class }}" role="article" aria-labelledby="park-title-grid-{{ park.id }}" aria-describedby="park-description-grid-{{ park.id }}">
{# Enhanced Park Image with CloudFlare Images Integration #} {# Enhanced Park Image with CloudFlare Images Integration #}
<div class="relative aspect-[4/3] bg-gradient-to-br from-blue-500 via-purple-500 to-pink-500 overflow-hidden"> <div class="relative aspect-[4/3] bg-gradient-to-br from-blue-500 via-purple-500 to-pink-500 overflow-hidden">
{% if park.card_image.image or park.photos.first.image %} {% if park.card_image.image or park.photos.first.image %}
@@ -258,7 +267,7 @@ Features:
</div> </div>
{% endif %} {% endif %}
{# Enhanced Status Badge Overlay with Better Mobile Touch Targets #} {# Enhanced Status Badge Overlay with Better Mobile Touch Targets and Accessibility #}
<div class="absolute top-2 right-2 sm:top-3 sm:right-3"> <div class="absolute top-2 right-2 sm:top-3 sm:right-3">
<span class="inline-flex items-center px-2 py-1 sm:px-2.5 sm:py-1 rounded-full text-xs font-semibold border shrink-0 bg-white/95 backdrop-blur-sm shadow-sm <span class="inline-flex items-center px-2 py-1 sm:px-2.5 sm:py-1 rounded-full text-xs font-semibold border shrink-0 bg-white/95 backdrop-blur-sm shadow-sm
{% if park.status == 'operating' or park.status == 'OPERATING' %}text-green-700 border-green-200 {% if park.status == 'operating' or park.status == 'OPERATING' %}text-green-700 border-green-200
@@ -268,8 +277,11 @@ Features:
{% elif park.status == 'under_construction' or park.status == 'UNDER_CONSTRUCTION' %}text-blue-700 border-blue-200 {% elif park.status == 'under_construction' or park.status == 'UNDER_CONSTRUCTION' %}text-blue-700 border-blue-200
{% elif park.status == 'demolished' or park.status == 'DEMOLISHED' %}text-gray-700 border-gray-200 {% elif park.status == 'demolished' or park.status == 'DEMOLISHED' %}text-gray-700 border-gray-200
{% elif park.status == 'relocated' or park.status == 'RELOCATED' %}text-purple-700 border-purple-200 {% elif park.status == 'relocated' or park.status == 'RELOCATED' %}text-purple-700 border-purple-200
{% else %}text-gray-700 border-gray-200{% endif %}"> {% else %}text-gray-700 border-gray-200{% endif %}"
{{ park.get_status_display }} role="img"
aria-label="Park status: {{ park.get_status_display }}"
title="Park status: {{ park.get_status_display }}">
<span aria-hidden="true">{{ park.get_status_display }}</span>
</span> </span>
</div> </div>
@@ -280,8 +292,8 @@ Features:
{# Enhanced Content Area with Better Mobile Optimization #} {# Enhanced Content Area with Better Mobile Optimization #}
<div class="p-4 sm:p-6"> <div class="p-4 sm:p-6">
<div class="mb-3 sm:mb-4"> <div class="mb-3 sm:mb-4">
{# Enhanced Title with Better Mobile Typography #} {# Enhanced Title with Better Mobile Typography and Accessibility #}
<h2 class="text-lg sm:text-xl font-bold line-clamp-2 mb-2 leading-tight"> <h3 id="park-title-grid-{{ park.id }}" class="text-lg sm:text-xl font-bold line-clamp-2 mb-2 leading-tight">
{% if park.slug %} {% if park.slug %}
<a href="{% url 'parks:park_detail' park.slug %}" <a href="{% url 'parks:park_detail' park.slug %}"
class="text-gray-900 dark:text-white hover:text-blue-600 dark:hover:text-blue-400 transition-all duration-300 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 rounded-sm" class="text-gray-900 dark:text-white hover:text-blue-600 dark:hover:text-blue-400 transition-all duration-300 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 rounded-sm"
@@ -293,7 +305,7 @@ Features:
{{ park.name }} {{ park.name }}
</span> </span>
{% endif %} {% endif %}
</h2> </h3>
</div> </div>
{# Enhanced Operator Display with Better Mobile Layout #} {# Enhanced Operator Display with Better Mobile Layout #}
@@ -306,9 +318,9 @@ Features:
</div> </div>
{% endif %} {% endif %}
{# Enhanced Description with Better Mobile Readability #} {# Enhanced Description with Better Mobile Readability and Accessibility #}
{% if park.description %} {% if park.description %}
<p class="text-sm text-gray-600 dark:text-gray-400 line-clamp-3 mb-4 leading-relaxed"> <p id="park-description-grid-{{ park.id }}" class="text-sm text-gray-600 dark:text-gray-400 line-clamp-3 mb-4 leading-relaxed">
{{ park.description|truncatewords:15 }} {{ park.description|truncatewords:15 }}
</p> </p>
{% endif %} {% endif %}
@@ -318,21 +330,27 @@ Features:
<div class="flex items-center justify-between pt-3 sm:pt-4 border-t border-gray-200/50 dark:border-gray-600/50"> <div class="flex items-center justify-between pt-3 sm:pt-4 border-t border-gray-200/50 dark:border-gray-600/50">
<div class="flex items-center space-x-3 sm:space-x-4 text-sm"> <div class="flex items-center space-x-3 sm:space-x-4 text-sm">
{% if park.ride_count %} {% if park.ride_count %}
<div class="flex items-center space-x-1.5 text-blue-600 dark:text-blue-400" title="{{ park.ride_count }} ride{{ park.ride_count|pluralize }}"> <div class="flex items-center space-x-1.5 text-blue-600 dark:text-blue-400"
<svg class="w-4 h-4 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"> role="img"
aria-label="{{ park.ride_count }} ride{{ park.ride_count|pluralize }} available"
title="{{ park.ride_count }} ride{{ park.ride_count|pluralize }}">
<svg class="w-4 h-4 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"/> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"/>
</svg> </svg>
<span class="font-semibold">{{ park.ride_count }}</span> <span class="font-semibold" aria-hidden="true">{{ park.ride_count }}</span>
<span class="hidden sm:inline text-xs opacity-75">rides</span> <span class="hidden sm:inline text-xs opacity-75" aria-hidden="true">rides</span>
</div> </div>
{% endif %} {% endif %}
{% if park.coaster_count %} {% if park.coaster_count %}
<div class="flex items-center space-x-1.5 text-purple-600 dark:text-purple-400" title="{{ park.coaster_count }} roller coaster{{ park.coaster_count|pluralize }}"> <div class="flex items-center space-x-1.5 text-purple-600 dark:text-purple-400"
<svg class="w-4 h-4 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"> role="img"
aria-label="{{ park.coaster_count }} roller coaster{{ park.coaster_count|pluralize }} available"
title="{{ park.coaster_count }} roller coaster{{ park.coaster_count|pluralize }}">
<svg class="w-4 h-4 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"/> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"/>
</svg> </svg>
<span class="font-semibold">{{ park.coaster_count }}</span> <span class="font-semibold" aria-hidden="true">{{ park.coaster_count }}</span>
<span class="hidden sm:inline text-xs opacity-75">coasters</span> <span class="hidden sm:inline text-xs opacity-75" aria-hidden="true">coasters</span>
</div> </div>
{% endif %} {% endif %}
</div> </div>

View File

@@ -44,7 +44,7 @@ Features:
class="" class=""
/> />
<div class="flex items-center justify-between text-sm text-gray-600 dark:text-gray-400 {{ class }}"> <div class="flex items-center justify-between text-sm text-gray-600 dark:text-gray-400 {{ class }}" role="status" aria-live="polite">
<div class="flex items-center gap-4"> <div class="flex items-center gap-4">
<!-- Result Count --> <!-- Result Count -->
<div class="flex items-center gap-1"> <div class="flex items-center gap-1">
@@ -74,11 +74,11 @@ Features:
<!-- Filter Indicator --> <!-- Filter Indicator -->
{% if filter_count and filter_count > 0 %} {% if filter_count and filter_count > 0 %}
<div class="flex items-center gap-1 text-blue-600 dark:text-blue-400"> <div class="flex items-center gap-1 text-blue-600 dark:text-blue-400" role="img" aria-label="{{ filter_count }} active filter{{ filter_count|pluralize }}">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 4a1 1 0 011-1h16a1 1 0 011 1v2.586a1 1 0 01-.293.707l-6.414 6.414a1 1 0 00-.293.707V17l-4 4v-6.586a1 1 0 00-.293-.707L3.293 7.293A1 1 0 013 6.586V4z" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 4a1 1 0 011-1h16a1 1 0 011 1v2.586a1 1 0 01-.293.707l-6.414 6.414a1 1 0 00-.293.707V17l-4 4v-6.586a1 1 0 00-.293-.707L3.293 7.293A1 1 0 013 6.586V4z" />
</svg> </svg>
<span>{{ filter_count }} filter{{ filter_count|pluralize }} active</span> <span aria-hidden="true">{{ filter_count }} filter{{ filter_count|pluralize }} active</span>
</div> </div>
{% endif %} {% endif %}
</div> </div>
@@ -103,7 +103,7 @@ Features:
{% if total_results == 0 and is_search %} {% if total_results == 0 and is_search %}
<div class="mt-3 p-3 bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 rounded-lg"> <div class="mt-3 p-3 bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 rounded-lg">
<div class="flex items-start gap-2"> <div class="flex items-start gap-2">
<svg class="w-5 h-5 mt-0.5 text-yellow-600 dark:text-yellow-400 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="w-5 h-5 mt-0.5 text-yellow-600 dark:text-yellow-400 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg> </svg>
<div class="text-sm"> <div class="text-sm">

View File

@@ -39,11 +39,12 @@ Features:
type="button" type="button"
class="inline-flex items-center justify-center w-full px-3 sm:px-4 py-2.5 sm:py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-lg shadow-sm hover:bg-gray-50 dark:hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-all duration-200 min-h-[44px] sm:min-h-0" class="inline-flex items-center justify-center w-full px-3 sm:px-4 py-2.5 sm:py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-lg shadow-sm hover:bg-gray-50 dark:hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-all duration-200 min-h-[44px] sm:min-h-0"
@click="open = !open" @click="open = !open"
aria-expanded="true" :aria-expanded="open"
aria-haspopup="true" aria-haspopup="true"
aria-label="Sort options" aria-label="Sort options menu"
id="sort-menu-button"
> >
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 4h13M3 8h9m-9 4h6m4 0l4-4m0 0l4 4m-4-4v12" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 4h13M3 8h9m-9 4h6m4 0l4-4m0 0l4 4m-4-4v12" />
</svg> </svg>
Sort by Sort by
@@ -72,7 +73,7 @@ Features:
<span class="ml-1">: {{ current_sort }}</span> <span class="ml-1">: {{ current_sort }}</span>
{% endif %} {% endif %}
{% endif %} {% endif %}
<svg class="w-5 h-5 ml-2 -mr-1" fill="currentColor" viewBox="0 0 20 20"> <svg class="w-5 h-5 ml-2 -mr-1" fill="currentColor" viewBox="0 0 20 20" aria-hidden="true">
<path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd" /> <path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd" />
</svg> </svg>
</button> </button>
@@ -90,18 +91,19 @@ Features:
@click.away="open = false" @click.away="open = false"
style="display: none;" style="display: none;"
> >
<div class="py-1" role="menu" aria-orientation="vertical" aria-labelledby="options-menu"> <div class="py-1" role="menu" aria-orientation="vertical" aria-labelledby="sort-menu-button">
{% if options %} {% if options %}
{% for option in options %} {% for option in options %}
<button <button
type="button" type="button"
class="flex items-center w-full px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700 {% if current_sort == option.value %}bg-blue-50 text-blue-700 dark:bg-blue-900/30 dark:text-blue-300{% endif %}" class="flex items-center w-full px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700 {% if current_sort == option.value %}bg-blue-50 text-blue-700 dark:bg-blue-900/30 dark:text-blue-300{% endif %} focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-inset"
hx-get="{% url 'parks:park_list' %}?{% for name, value in request.GET.items %}{% if name != 'ordering' and name != 'page' %}{{ name }}={{ value }}&{% endif %}{% endfor %}ordering={{ option.value }}" hx-get="{% url 'parks:park_list' %}?{% for name, value in request.GET.items %}{% if name != 'ordering' and name != 'page' %}{{ name }}={{ value }}&{% endif %}{% endfor %}ordering={{ option.value }}"
hx-target="#park-results" hx-target="#park-results"
hx-push-url="true" hx-push-url="true"
hx-indicator="#search-spinner" hx-indicator="#search-spinner"
@click="open = false" @click="open = false"
role="menuitem" role="menuitem"
tabindex="-1"
> >
{% if current_sort == option.value %} {% if current_sort == option.value %}
<svg class="w-4 h-4 mr-2 text-blue-600" fill="currentColor" viewBox="0 0 20 20"> <svg class="w-4 h-4 mr-2 text-blue-600" fill="currentColor" viewBox="0 0 20 20">

View File

@@ -30,7 +30,7 @@ Features:
class="" class=""
/> />
<div class="inline-flex rounded-lg border border-gray-200 dark:border-gray-700 {{ class }}" role="group" aria-label="View toggle"> <div class="inline-flex rounded-lg border border-gray-200 dark:border-gray-700 {{ class }}" role="group" aria-label="Toggle between grid and list view modes">
<button <button
type="button" type="button"
class="inline-flex items-center px-3 py-2.5 sm:py-2 text-sm font-medium rounded-l-lg transition-all duration-200 min-h-[44px] sm:min-h-0 {% if current_view == 'grid' %}bg-blue-50 text-blue-700 border-blue-200 dark:bg-blue-900/30 dark:text-blue-300 dark:border-blue-700{% else %}bg-white text-gray-700 hover:bg-gray-50 focus:bg-gray-50 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700 dark:focus:bg-gray-700{% endif %} focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2" class="inline-flex items-center px-3 py-2.5 sm:py-2 text-sm font-medium rounded-l-lg transition-all duration-200 min-h-[44px] sm:min-h-0 {% if current_view == 'grid' %}bg-blue-50 text-blue-700 border-blue-200 dark:bg-blue-900/30 dark:text-blue-300 dark:border-blue-700{% else %}bg-white text-gray-700 hover:bg-gray-50 focus:bg-gray-50 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700 dark:focus:bg-gray-700{% endif %} focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
@@ -42,7 +42,7 @@ Features:
aria-pressed="{% if current_view == 'grid' %}true{% else %}false{% endif %}" aria-pressed="{% if current_view == 'grid' %}true{% else %}false{% endif %}"
title="Grid view" title="Grid view"
> >
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zM14 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2zM14 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zM14 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2zM14 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z" />
</svg> </svg>
<span class="ml-1 hidden sm:inline">Grid</span> <span class="ml-1 hidden sm:inline">Grid</span>
@@ -59,7 +59,7 @@ Features:
aria-pressed="{% if current_view == 'list' %}true{% else %}false{% endif %}" aria-pressed="{% if current_view == 'list' %}true{% else %}false{% endif %}"
title="List view" title="List view"
> >
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 10h16M4 14h16M4 18h16" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 10h16M4 14h16M4 18h16" />
</svg> </svg>
<span class="ml-1 hidden sm:inline">List</span> <span class="ml-1 hidden sm:inline">List</span>

View File

@@ -5,46 +5,57 @@
{% block title %}Parks{% endblock %} {% block title %}Parks{% endblock %}
{% block content %} {% block content %}
{# Enhanced Mobile-First Container with Better Spacing #} {# Skip Navigation Links for Keyboard Users #}
<a href="#main-content" class="sr-only focus:not-sr-only focus:absolute focus:top-4 focus:left-4 z-50 px-4 py-2 bg-blue-600 text-white font-semibold rounded-lg shadow-lg focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-all duration-200">
Skip to main content
</a>
<a href="#search-form" class="sr-only focus:not-sr-only focus:absolute focus:top-4 focus:left-32 z-50 px-4 py-2 bg-blue-600 text-white font-semibold rounded-lg shadow-lg focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-all duration-200">
Skip to search
</a>
{# Enhanced Mobile-First Container with Better Spacing and Landmarks #}
<div class="container mx-auto px-3 sm:px-4 lg:px-6 py-4 sm:py-6" x-data="parkListState()"> <div class="container mx-auto px-3 sm:px-4 lg:px-6 py-4 sm:py-6" x-data="parkListState()">
{# Enhanced Mobile-First Header Section #} {# Enhanced Mobile-First Header Section #}
<div class="mb-6 sm:mb-8"> <header class="mb-6 sm:mb-8" aria-labelledby="page-title">
<div class="flex flex-col gap-4 sm:gap-6"> <div class="flex flex-col gap-4 sm:gap-6">
{# Enhanced Mobile-First Title Section #} {# Enhanced Mobile-First Title Section with Proper Heading #}
<div class="text-center sm:text-left"> <div class="text-center sm:text-left">
<h1 class="text-2xl sm:text-3xl lg:text-4xl font-bold text-gray-900 dark:text-white leading-tight"> <h1 id="page-title" class="text-2xl sm:text-3xl lg:text-4xl font-bold text-gray-900 dark:text-white leading-tight">
Theme Parks Theme Parks
</h1> </h1>
<p class="mt-1 sm:mt-2 text-base sm:text-lg text-gray-600 dark:text-gray-400"> <p class="mt-1 sm:mt-2 text-base sm:text-lg text-gray-600 dark:text-gray-400" id="page-description">
Discover amazing theme parks around the world Discover amazing theme parks around the world
</p> </p>
</div> </div>
{# Enhanced Mobile-First Quick Stats with Better Touch Targets #} {# Enhanced Mobile-First Quick Stats with Better Touch Targets and Landmarks #}
<div class="grid grid-cols-3 gap-3 sm:gap-4 lg:gap-6"> <section aria-labelledby="park-statistics" class="grid grid-cols-3 gap-3 sm:gap-4 lg:gap-6">
<div class="text-center p-3 sm:p-4 bg-white/50 dark:bg-gray-800/50 rounded-lg border border-gray-200/50 dark:border-gray-700/50"> <h2 id="park-statistics" class="sr-only">Park Statistics Summary</h2>
<div class="font-bold text-lg sm:text-xl lg:text-2xl text-gray-900 dark:text-white">{{ filter_counts.total_parks|default:0 }}</div> <div class="text-center p-3 sm:p-4 bg-white/50 dark:bg-gray-800/50 rounded-lg border border-gray-200/50 dark:border-gray-700/50" role="img" aria-labelledby="total-parks-stat" tabindex="0">
<div id="total-parks-stat" class="font-bold text-lg sm:text-xl lg:text-2xl text-gray-900 dark:text-white" aria-label="{{ filter_counts.total_parks|default:0 }} total parks in database">{{ filter_counts.total_parks|default:0 }}</div>
<div class="text-xs sm:text-sm text-gray-600 dark:text-gray-400 mt-1">Total Parks</div> <div class="text-xs sm:text-sm text-gray-600 dark:text-gray-400 mt-1">Total Parks</div>
</div> </div>
<div class="text-center p-3 sm:p-4 bg-white/50 dark:bg-gray-800/50 rounded-lg border border-gray-200/50 dark:border-gray-700/50"> <div class="text-center p-3 sm:p-4 bg-white/50 dark:bg-gray-800/50 rounded-lg border border-gray-200/50 dark:border-gray-700/50" role="img" aria-labelledby="operating-parks-stat" tabindex="0">
<div class="font-bold text-lg sm:text-xl lg:text-2xl text-gray-900 dark:text-white">{{ filter_counts.operating_parks|default:0 }}</div> <div id="operating-parks-stat" class="font-bold text-lg sm:text-xl lg:text-2xl text-gray-900 dark:text-white" aria-label="{{ filter_counts.operating_parks|default:0 }} currently operating parks">{{ filter_counts.operating_parks|default:0 }}</div>
<div class="text-xs sm:text-sm text-gray-600 dark:text-gray-400 mt-1">Operating</div> <div class="text-xs sm:text-sm text-gray-600 dark:text-gray-400 mt-1">Operating</div>
</div> </div>
<div class="text-center p-3 sm:p-4 bg-white/50 dark:bg-gray-800/50 rounded-lg border border-gray-200/50 dark:border-gray-700/50"> <div class="text-center p-3 sm:p-4 bg-white/50 dark:bg-gray-800/50 rounded-lg border border-gray-200/50 dark:border-gray-700/50" role="img" aria-labelledby="coaster-parks-stat" tabindex="0">
<div class="font-bold text-lg sm:text-xl lg:text-2xl text-gray-900 dark:text-white">{{ filter_counts.parks_with_coasters|default:0 }}</div> <div id="coaster-parks-stat" class="font-bold text-lg sm:text-xl lg:text-2xl text-gray-900 dark:text-white" aria-label="{{ filter_counts.parks_with_coasters|default:0 }} parks with roller coasters">{{ filter_counts.parks_with_coasters|default:0 }}</div>
<div class="text-xs sm:text-sm text-gray-600 dark:text-gray-400 mt-1">With Coasters</div> <div class="text-xs sm:text-sm text-gray-600 dark:text-gray-400 mt-1">With Coasters</div>
</div> </div>
</div> </section>
</div> </div>
</div> </header>
{# Enhanced Mobile-First Search and Filter Bar #} {# Enhanced Mobile-First Search and Filter Bar with Proper Landmarks #}
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-lg border border-gray-200 dark:border-gray-700 p-4 sm:p-6 mb-6 sm:mb-8"> <section class="bg-white dark:bg-gray-800 rounded-xl shadow-lg border border-gray-200 dark:border-gray-700 p-4 sm:p-6 mb-6 sm:mb-8" aria-labelledby="search-filters-heading" role="search">
<h2 id="search-filters-heading" class="sr-only">Search and Filter Parks</h2>
<div class="space-y-4 sm:space-y-6"> <div class="space-y-4 sm:space-y-6">
{# Enhanced Mobile-First Main Search Row #} {# Enhanced Mobile-First Main Search Row #}
<div class="space-y-3 sm:space-y-0 sm:flex sm:flex-col lg:flex-row gap-4"> <div class="space-y-3 sm:space-y-0 sm:flex sm:flex-col lg:flex-row gap-4">
{# Enhanced Search Input with Better Mobile UX #} {# Enhanced Search Input with Better Mobile UX and Form Landmark #}
<div class="flex-1"> <div class="flex-1" id="search-form">
<label for="park-search" class="sr-only">Search parks by name, location, or features</label>
<c-enhanced_search <c-enhanced_search
placeholder="Search parks by name, location, or features..." placeholder="Search parks by name, location, or features..."
current_value="{{ search_query }}" current_value="{{ search_query }}"
@@ -53,8 +64,8 @@
/> />
</div> </div>
{# Enhanced Mobile-First Controls Row with Better Touch Targets #} {# Enhanced Mobile-First Controls Row with Better Touch Targets and Navigation #}
<div class="flex items-center justify-between sm:justify-start gap-2 sm:gap-3"> <nav class="flex items-center justify-between sm:justify-start gap-2 sm:gap-3" aria-label="View and sort controls">
{# Sort Controls with Mobile Optimization #} {# Sort Controls with Mobile Optimization #}
<div class="flex-1 sm:flex-none min-w-0"> <div class="flex-1 sm:flex-none min-w-0">
<c-sort_controls <c-sort_controls
@@ -88,7 +99,7 @@
<span class="ml-1 sm:ml-2 hidden sm:inline">Filters</span> <span class="ml-1 sm:ml-2 hidden sm:inline">Filters</span>
<span class="sr-only sm:hidden" x-text="showFilters ? 'Hide filters' : 'Show filters'"></span> <span class="sr-only sm:hidden" x-text="showFilters ? 'Hide filters' : 'Show filters'"></span>
</button> </button>
</div> </nav>
</div> </div>
{# Enhanced Mobile-First Advanced Filters with Better Touch Interaction #} {# Enhanced Mobile-First Advanced Filters with Better Touch Interaction #}