diff --git a/app/assets/stylesheets/timeline_events.css b/app/assets/stylesheets/timeline_events.css index 056862c5..a027fed0 100644 --- a/app/assets/stylesheets/timeline_events.css +++ b/app/assets/stylesheets/timeline_events.css @@ -1,141 +1,343 @@ -/* Timeline Event Color Coding and Visual Hierarchy */ +/* ========================= + MODERN TIMELINE EDITOR + Paper-Style Card System + ========================= */ -/* Event Type Color Coding */ -.timeline-event-gray { border-left: 4px solid #6B7280; } -.timeline-event-blue { border-left: 4px solid #3B82F6; } -.timeline-event-green { border-left: 4px solid #10B981; } -.timeline-event-purple { border-left: 4px solid #8B5CF6; } -.timeline-event-amber { border-left: 4px solid #F59E0B; } -.timeline-event-red { border-left: 4px solid #EF4444; } -.timeline-event-indigo { border-left: 4px solid #6366F1; } -.timeline-event-pink { border-left: 4px solid #EC4899; } -.timeline-event-yellow { border-left: 4px solid #EAB308; } -.timeline-event-teal { border-left: 4px solid #14B8A6; } - -/* Event Type Background Tints */ -.timeline-event-gray .event-type-badge { @apply bg-gray-100 text-gray-800; } -.timeline-event-blue .event-type-badge { @apply bg-blue-100 text-blue-800; } -.timeline-event-green .event-type-badge { @apply bg-green-100 text-green-800; } -.timeline-event-purple .event-type-badge { @apply bg-purple-100 text-purple-800; } -.timeline-event-amber .event-type-badge { @apply bg-amber-100 text-amber-800; } -.timeline-event-red .event-type-badge { @apply bg-red-100 text-red-800; } -.timeline-event-indigo .event-type-badge { @apply bg-indigo-100 text-indigo-800; } -.timeline-event-pink .event-type-badge { @apply bg-pink-100 text-pink-800; } -.timeline-event-yellow .event-type-badge { @apply bg-yellow-100 text-yellow-800; } -.timeline-event-teal .event-type-badge { @apply bg-teal-100 text-teal-800; } - -/* Importance Level Visual Hierarchy */ -.timeline-event-major { - box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); - border-width: 2px; +/* Container Layout System */ +.timeline-events-container { + position: relative; + min-height: 200px; } -.timeline-event-major .event-title { - font-size: 1.25rem; - font-weight: 700; +/* Timeline Spine - Continuous Vertical Rail */ +.timeline-spine { + background: linear-gradient(to bottom, + transparent 0%, + #d1d5db 10%, + #9ca3af 50%, + #d1d5db 90%, + transparent 100% + ); + box-shadow: inset 0 0 1px rgba(0, 0, 0, 0.1); } -.timeline-event-minor { - box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06); +/* Timeline Dots - Connected to Spine */ +.timeline-dot { + transition: all 0.2s ease-in-out; + position: relative; + z-index: 10; } -.timeline-event-minor .event-title { - font-size: 1.125rem; - font-weight: 600; +.timeline-dot:hover { + transform: scale(1.1); + box-shadow: 0 4px 12px rgba(16, 185, 129, 0.3); } -.timeline-event-background { - opacity: 0.85; - border-style: dashed; +.timeline-dot::before { + content: ''; + position: absolute; + inset: -2px; + border-radius: 50%; + z-index: -1; } -.timeline-event-background .event-title { - font-size: 1rem; - font-weight: 500; - color: #6B7280; +/* Paper-Style Event Cards */ +.timeline-event-card { + background: white; + border: 1px solid #e5e7eb; + border-radius: 16px; + box-shadow: + 0 1px 3px 0 rgba(0, 0, 0, 0.1), + 0 1px 2px 0 rgba(0, 0, 0, 0.06); + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + position: relative; + overflow: visible; } -/* Status Indicators */ -.status-badge-planned { @apply bg-yellow-100 text-yellow-800; } -.status-badge-happening { @apply bg-blue-100 text-blue-800; } -.status-badge-completed { @apply bg-green-100 text-green-800; } -.status-badge-cancelled { @apply bg-gray-100 text-gray-800; } +/* Selected Event State */ +.timeline-event-card.ring-2 { + border-color: #10b981; + box-shadow: + 0 0 0 2px rgba(16, 185, 129, 0.2), + 0 4px 6px -1px rgba(0, 0, 0, 0.1), + 0 2px 4px -1px rgba(0, 0, 0, 0.06); + transform: translateY(-1px) scale(1.01); +} -/* Timeline Dot Colors */ -.timeline-dot-gray { background-color: #6B7280; } -.timeline-dot-blue { background-color: #3B82F6; } -.timeline-dot-green { background-color: #10B981; } -.timeline-dot-purple { background-color: #8B5CF6; } -.timeline-dot-amber { background-color: #F59E0B; } -.timeline-dot-red { background-color: #EF4444; } -.timeline-dot-indigo { background-color: #6366F1; } -.timeline-dot-pink { background-color: #EC4899; } -.timeline-dot-yellow { background-color: #EAB308; } -.timeline-dot-teal { background-color: #14B8A6; } +.timeline-event-card.ring-2::before { + opacity: 1; +} -/* Duration Events */ -.duration-event { +.timeline-event-card::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 4px; + background: linear-gradient(90deg, + #10b981 0%, + #059669 50%, + #10b981 100% + ); + opacity: 0; + transition: opacity 0.3s ease; +} + +.timeline-event-card:hover { + transform: translateY(-2px) scale(1.02); + box-shadow: + 0 10px 25px -3px rgba(0, 0, 0, 0.1), + 0 4px 6px -2px rgba(0, 0, 0, 0.05); + border-color: #d1d5db; +} + +.timeline-event-card:hover::before { + opacity: 1; +} + +/* Card Header Styling */ +.timeline-event-card .px-6.py-4.border-b { + background: linear-gradient(135deg, #fefefe 0%, #f8fafc 100%); + border-bottom: 1px solid #f1f5f9; + border-radius: 16px 16px 0 0; position: relative; } -.duration-event::after { +.timeline-event-card .px-6.py-4.border-b::after { content: ''; position: absolute; - left: 32px; - top: 0; bottom: 0; - width: 2px; - background: linear-gradient(to bottom, currentColor 0%, transparent 100%); - opacity: 0.3; + left: 0; + right: 0; + height: 1px; + background: linear-gradient(90deg, + transparent 0%, + #e2e8f0 50%, + transparent 100% + ); } -/* Filter Sidebar */ -.timeline-filters { - position: sticky; - top: 120px; - max-height: calc(100vh - 140px); - overflow-y: auto; +/* Event Title Input Styling */ +.timeline-event-card input[name*="[title]"] { + font-weight: 600; + color: #1f2937; + background: transparent; + border: none; + border-bottom: 2px solid transparent; + padding: 0.25rem 0; + transition: all 0.2s ease; } -.filter-group { - margin-bottom: 1.5rem; +.timeline-event-card input[name*="[title]"]:hover { + border-bottom-color: #e5e7eb; } -.filter-checkbox { - display: flex; - align-items: center; +.timeline-event-card input[name*="[title]"]:focus { + border-bottom-color: #10b981; + box-shadow: 0 2px 4px rgba(16, 185, 129, 0.1); +} + +/* Time Label Input Styling */ +.timeline-event-card input[name*="[time_label]"] { + color: #6b7280; + font-size: 0.875rem; + background: transparent; + border: none; + border-bottom: 1px solid transparent; + padding: 0.125rem 0; + transition: all 0.2s ease; +} + +.timeline-event-card input[name*="[time_label]"]:hover { + border-bottom-color: #d1d5db; +} + +.timeline-event-card input[name*="[time_label]"]:focus { + border-bottom-color: #10b981; + color: #374151; +} + +/* Description Textarea Styling */ +.timeline-event-card textarea[name*="[description]"] { + background: #fefefe; + border: 1px solid #f3f4f6; + border-radius: 12px; + padding: 0.875rem 1rem; + transition: all 0.2s ease; + font-size: 0.9375rem; + line-height: 1.6; +} + +.timeline-event-card textarea[name*="[description]"]:hover { + background: #ffffff; + border-color: #e5e7eb; +} + +.timeline-event-card textarea[name*="[description]"]:focus { + background: #ffffff; + border-color: #10b981; + box-shadow: + 0 0 0 3px rgba(16, 185, 129, 0.1), + 0 2px 4px rgba(0, 0, 0, 0.05); +} + +/* Action Button Styling */ +.timeline-move-controls button { + background: transparent; + border: none; padding: 0.5rem; - border-radius: 0.5rem; - transition: background-color 0.2s; + border-radius: 8px; + color: #9ca3af; + transition: all 0.2s ease; + position: relative; } -.filter-checkbox:hover { - background-color: #F9FAFB; +.timeline-move-controls button:hover { + background: rgba(16, 185, 129, 0.1); + color: #10b981; + transform: scale(1.1); } -.filter-checkbox input[type="checkbox"] { - margin-right: 0.75rem; +.timeline-move-controls button:hover[onclick*="delete"] { + background: rgba(239, 68, 68, 0.1); + color: #ef4444; } -.filter-color-dot { - width: 12px; - height: 12px; - border-radius: 50%; - margin-right: 0.5rem; +/* Action Dropdown Menu */ +.timeline-move-controls .absolute.right-0 { + background: white; + border: 1px solid #e5e7eb; + border-radius: 12px; + box-shadow: + 0 10px 15px -3px rgba(0, 0, 0, 0.1), + 0 4px 6px -2px rgba(0, 0, 0, 0.05); + overflow: hidden; +} + +.timeline-move-controls .absolute.right-0 a { + padding: 0.75rem 1rem; + color: #374151; + transition: all 0.15s ease; + border-bottom: 1px solid #f9fafb; +} + +.timeline-move-controls .absolute.right-0 a:hover { + background: #f8fafc; + color: #10b981; +} + +.timeline-move-controls .absolute.right-0 a:last-child { + border-bottom: none; +} + +/* Empty State Styling */ +.text-center.py-16 { + animation: fadeInUp 0.6s ease-out; +} + +@keyframes fadeInUp { + from { + opacity: 0; + transform: translateY(20px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.text-center.py-16 .mx-auto.h-24 { + background: linear-gradient(135deg, #d1fae5 0%, #a7f3d0 100%); + box-shadow: + 0 4px 6px -1px rgba(0, 0, 0, 0.1), + 0 2px 4px -1px rgba(0, 0, 0, 0.06); + transition: all 0.3s ease; +} + +.text-center.py-16:hover .mx-auto.h-24 { + transform: scale(1.05); + box-shadow: + 0 10px 15px -3px rgba(0, 0, 0, 0.1), + 0 4px 6px -2px rgba(0, 0, 0, 0.05); +} + +/* Form Focus States */ +.timeline-event-card input:focus, +.timeline-event-card textarea:focus { + outline: none; +} + +/* Loading Animation for New Events */ +.timeline-event-container.creating { + animation: slideInFromTop 0.4s ease-out; +} + +@keyframes slideInFromTop { + from { + opacity: 0; + transform: translateY(-20px) scale(0.95); + } + to { + opacity: 1; + transform: translateY(0) scale(1); + } +} + +/* Hover Group Effects */ +.timeline-event-container:hover .timeline-dot { + transform: scale(1.15); + box-shadow: 0 6px 20px rgba(16, 185, 129, 0.4); +} + +/* Card Content Spacing */ +.timeline-event-card .space-y-4 > * + * { + margin-top: 1.5rem; +} + +/* Subtle Background Pattern */ +.timeline-event-card .px-6.py-4.space-y-4 { + background-image: + radial-gradient(circle at 1px 1px, rgba(16, 185, 129, 0.03) 1px, transparent 0); + background-size: 20px 20px; + background-position: 0 0; +} + +/* Paper Texture Effect */ +.timeline-event-card { + background-image: + linear-gradient(45deg, transparent 48%, rgba(255,255,255,0.8) 49%, rgba(255,255,255,0.8) 51%, transparent 52%), + linear-gradient(-45deg, transparent 48%, rgba(255,255,255,0.6) 49%, rgba(255,255,255,0.6) 51%, transparent 52%); + background-size: 3px 3px; +} + +/* Responsive Design */ +@media (max-width: 1024px) { + .timeline-event-card { + margin-left: 0; + border-radius: 12px; + } + + .timeline-spine { + display: none; + } + + .timeline-dot { + position: relative; + left: auto; + top: auto; + margin-bottom: 1rem; + } } -/* Responsive Adjustments */ @media (max-width: 768px) { - .timeline-event-major .event-title { - font-size: 1.125rem; + .timeline-event-card { + border-radius: 8px; + margin-bottom: 1rem; } - .timeline-event-minor .event-title { - font-size: 1rem; + .timeline-event-card .px-6.py-4 { + padding: 1rem 1.25rem; } - - .timeline-filters { - position: static; - max-height: none; - } -} \ No newline at end of file +} + diff --git a/app/assets/stylesheets/timeline_inspector.css b/app/assets/stylesheets/timeline_inspector.css new file mode 100644 index 00000000..526dbc6b --- /dev/null +++ b/app/assets/stylesheets/timeline_inspector.css @@ -0,0 +1,296 @@ +/* ========================= + TIMELINE INSPECTOR PANEL + Modern Details & Linking UI + ========================= */ + +/* Inspector Panel Container */ +.timeline-inspector { + background: linear-gradient(135deg, #ffffff 0%, #fefefe 100%); + border-left: 1px solid #e5e7eb; + box-shadow: + inset 1px 0 0 0 rgba(255, 255, 255, 0.5), + -2px 0 8px -2px rgba(0, 0, 0, 0.05); + position: relative; + overflow: hidden; +} + +.timeline-inspector::before { + content: ''; + position: absolute; + top: 0; + left: 0; + bottom: 0; + width: 1px; + background: linear-gradient(180deg, + transparent 0%, + rgba(16, 185, 129, 0.1) 50%, + transparent 100% + ); +} + +/* Inspector Header */ +.timeline-inspector .px-6.py-4.border-b { + background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%); + border-bottom: 1px solid #e2e8f0; + position: relative; +} + +.timeline-inspector .px-6.py-4.border-b::after { + content: ''; + position: absolute; + bottom: 0; + left: 0; + right: 0; + height: 1px; + background: linear-gradient(90deg, + transparent 0%, + #cbd5e1 50%, + transparent 100% + ); +} + +/* Event Title in Inspector */ +.timeline-inspector h4 { + color: #1f2937; + font-weight: 600; + background: linear-gradient(135deg, #10b981 0%, #059669 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +/* Metadata Badges */ +.timeline-inspector .px-3.py-1 { + transition: all 0.2s ease; + backdrop-filter: blur(8px); + border: 1px solid transparent; +} + +.timeline-inspector .bg-blue-100 { + background: linear-gradient(135deg, #dbeafe 0%, #bfdbfe 100%); + border-color: rgba(59, 130, 246, 0.2); +} + +.timeline-inspector .bg-yellow-100 { + background: linear-gradient(135deg, #fef3c7 0%, #fde68a 100%); + border-color: rgba(245, 158, 11, 0.2); +} + +/* Description Panel */ +.timeline-inspector .bg-gray-50 { + background: linear-gradient(135deg, #f9fafb 0%, #f3f4f6 100%); + border: 1px solid #e5e7eb; + transition: all 0.2s ease; +} + +.timeline-inspector .bg-gray-50:hover { + background: linear-gradient(135deg, #ffffff 0%, #f9fafb 100%); + transform: translateY(-1px); + box-shadow: + 0 2px 4px 0 rgba(0, 0, 0, 0.05), + 0 1px 2px 0 rgba(0, 0, 0, 0.05); +} + +/* Quick Action Buttons */ +.timeline-inspector button { + transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); + position: relative; + overflow: hidden; +} + +.timeline-inspector button::before { + content: ''; + position: absolute; + top: 0; + left: -100%; + width: 100%; + height: 100%; + background: linear-gradient(90deg, + transparent 0%, + rgba(255, 255, 255, 0.2) 50%, + transparent 100% + ); + transition: left 0.5s ease; +} + +.timeline-inspector button:hover::before { + left: 100%; +} + +/* Link Content Button */ +.timeline-inspector .bg-green-100 { + background: linear-gradient(135deg, #d1fae5 0%, #a7f3d0 100%); + border: 1px solid #6ee7b7; + box-shadow: + 0 1px 2px 0 rgba(16, 185, 129, 0.1), + inset 0 1px 0 0 rgba(255, 255, 255, 0.1); +} + +.timeline-inspector .bg-green-100:hover { + background: linear-gradient(135deg, #a7f3d0 0%, #6ee7b7 100%); + transform: translateY(-1px); + box-shadow: + 0 2px 4px 0 rgba(16, 185, 129, 0.2), + 0 1px 2px 0 rgba(0, 0, 0, 0.05); +} + +/* Delete Button */ +.timeline-inspector .bg-red-50 { + background: linear-gradient(135deg, #fef2f2 0%, #fecaca 100%); + border: 1px solid #fca5a5; + box-shadow: + 0 1px 2px 0 rgba(239, 68, 68, 0.1), + inset 0 1px 0 0 rgba(255, 255, 255, 0.1); +} + +.timeline-inspector .bg-red-50:hover { + background: linear-gradient(135deg, #fecaca 0%, #f87171 100%); + transform: translateY(-1px); + box-shadow: + 0 2px 4px 0 rgba(239, 68, 68, 0.2), + 0 1px 2px 0 rgba(0, 0, 0, 0.05); +} + +/* Empty State Styling */ +.timeline-inspector .text-gray-500 { + animation: fadeIn 0.3s ease-out; +} + +@keyframes fadeIn { + from { + opacity: 0; + transform: translateY(10px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +/* Linked Content Info Box */ +.timeline-inspector .bg-gray-50.rounded-lg { + background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%); + border: 1px solid #e2e8f0; + border-left: 3px solid #10b981; +} + +/* Section Dividers */ +.timeline-inspector .border-t, +.timeline-inspector .border-b { + border-color: #f1f5f9; +} + +/* Scrollbar Styling */ +.timeline-inspector .overflow-y-auto::-webkit-scrollbar { + width: 6px; +} + +.timeline-inspector .overflow-y-auto::-webkit-scrollbar-track { + background: transparent; +} + +.timeline-inspector .overflow-y-auto::-webkit-scrollbar-thumb { + background: rgba(156, 163, 175, 0.3); + border-radius: 3px; +} + +.timeline-inspector .overflow-y-auto::-webkit-scrollbar-thumb:hover { + background: rgba(156, 163, 175, 0.5); +} + +/* Mobile Responsive */ +@media (max-width: 1280px) { + .timeline-inspector { + position: fixed; + top: 0; + right: -100%; + width: 24rem; + height: 100vh; + z-index: 50; + transition: right 0.3s ease; + box-shadow: + -2px 0 10px 0 rgba(0, 0, 0, 0.1), + 0 0 0 1px rgba(0, 0, 0, 0.05); + } + + .timeline-inspector.open { + right: 0; + } + + /* Mobile backdrop */ + .timeline-inspector-backdrop { + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.5); + z-index: 40; + opacity: 0; + pointer-events: none; + transition: opacity 0.3s ease; + } + + .timeline-inspector-backdrop.open { + opacity: 1; + pointer-events: auto; + } +} + +/* Focus States */ +.timeline-inspector button:focus { + outline: 2px solid #10b981; + outline-offset: 2px; +} + +/* Loading States */ +.timeline-inspector button:disabled { + opacity: 0.6; + cursor: not-allowed; + transform: none !important; +} + +.timeline-inspector button:disabled:hover { + transform: none !important; + box-shadow: none !important; +} + +/* Subtle pattern overlay */ +.timeline-inspector::after { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-image: + radial-gradient(circle at 1px 1px, rgba(16, 185, 129, 0.02) 1px, transparent 0); + background-size: 20px 20px; + pointer-events: none; + z-index: 1; +} + +.timeline-inspector > * { + position: relative; + z-index: 2; +} + +/* Animation for content changes */ +.timeline-inspector [x-show] { + transition: all 0.2s ease-out; +} + +/* Enhanced section headers */ +.timeline-inspector h5 { + position: relative; + color: #1f2937; + font-weight: 600; +} + +.timeline-inspector h5::after { + content: ''; + position: absolute; + bottom: -0.25rem; + left: 0; + width: 1.5rem; + height: 2px; + background: linear-gradient(90deg, #10b981 0%, transparent 100%); + border-radius: 1px; +} \ No newline at end of file diff --git a/app/assets/stylesheets/timeline_linked_content.css b/app/assets/stylesheets/timeline_linked_content.css index c9530abf..b84e255c 100644 --- a/app/assets/stylesheets/timeline_linked_content.css +++ b/app/assets/stylesheets/timeline_linked_content.css @@ -1,4 +1,7 @@ -/* Timeline Event Linked Content Cards */ +/* ========================= + TIMELINE LINKED CONTENT + Paper-Style Card Integration + ========================= */ /* Horizontal Scrolling Container */ .linked-content-scroll { @@ -6,23 +9,12 @@ scrollbar-color: rgba(156, 163, 175, 0.3) transparent; } -/* Breakout container to escape parent padding constraints */ +/* Contained scrolling for linked content */ .linked-content-breakout { - /* Ensure full width utilization */ - width: 100vw; - max-width: calc(100vw - 2rem); /* Account for page margins */ - margin-left: calc(-50vw + 50%); - margin-right: calc(-50vw + 50%); -} - -@media (min-width: 1280px) { - .linked-content-breakout { - /* Constrain to container on larger screens */ - width: auto; - max-width: none; - margin-left: -1.5rem; - margin-right: -1.5rem; - } + /* Stay within parent container bounds */ + width: 100%; + margin-left: 0; + margin-right: 0; } .linked-content-scroll::-webkit-scrollbar { @@ -64,14 +56,45 @@ overflow: hidden; } -/* Compact Card Hover Effects */ +/* Linked Content Card Styling */ .linked-content-card { - transition: all 0.2s ease-in-out; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + border-radius: 12px; + background: white; + border: 1px solid #f3f4f6; + box-shadow: + 0 1px 3px 0 rgba(0, 0, 0, 0.1), + 0 1px 2px 0 rgba(0, 0, 0, 0.06); + position: relative; + overflow: hidden; +} + +.linked-content-card::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 2px; + background: linear-gradient(90deg, + #10b981 0%, + #059669 50%, + #10b981 100% + ); + opacity: 0; + transition: opacity 0.3s ease; } .linked-content-card:hover { - transform: translateY(-2px) scale(1.02); - box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15); + transform: translateY(-3px) scale(1.03); + box-shadow: + 0 10px 25px -3px rgba(0, 0, 0, 0.1), + 0 4px 6px -2px rgba(0, 0, 0, 0.05); + border-color: #e5e7eb; +} + +.linked-content-card:hover::before { + opacity: 1; } /* Image Placeholder Gradient */ @@ -79,65 +102,117 @@ background: linear-gradient(135deg, #f3f4f6 0%, #e5e7eb 100%); } -/* Enhanced Badge Styles */ +/* Modern Badge Styles */ .type-badge { - backdrop-filter: blur(12px); - -webkit-backdrop-filter: blur(12px); - border: 1px solid rgba(255, 255, 255, 0.2); + background: rgba(255, 255, 255, 0.95); + border: 1px solid rgba(229, 231, 235, 0.8); + box-shadow: + 0 1px 3px 0 rgba(0, 0, 0, 0.1), + 0 1px 2px 0 rgba(0, 0, 0, 0.06); + backdrop-filter: blur(8px); + border-radius: 8px; + transition: all 0.2s ease; } -/* Remove Button Hover Effects */ +.linked-content-card:hover .type-badge { + background: rgba(255, 255, 255, 1); + transform: translateY(-1px); + box-shadow: + 0 4px 6px -1px rgba(0, 0, 0, 0.1), + 0 2px 4px -1px rgba(0, 0, 0, 0.06); +} + +/* Modern Remove Button */ .remove-btn { transition: all 0.2s ease-in-out; - backdrop-filter: blur(12px); - -webkit-backdrop-filter: blur(12px); - border: 1px solid rgba(255, 255, 255, 0.2); + background: rgba(255, 255, 255, 0.95); + border: 1px solid rgba(229, 231, 235, 0.8); + box-shadow: + 0 1px 3px 0 rgba(0, 0, 0, 0.1), + 0 1px 2px 0 rgba(0, 0, 0, 0.06); + backdrop-filter: blur(8px); + border-radius: 8px; } .remove-btn:hover { - transform: scale(1.1); + transform: scale(1.1) translateY(-1px); + background: #fef2f2; + border-color: #fecaca; + color: #dc2626; + box-shadow: + 0 4px 6px -1px rgba(0, 0, 0, 0.1), + 0 2px 4px -1px rgba(0, 0, 0, 0.06); } -/* Name Card Overlay Styling */ +/* Clean Name Card Overlay */ .name-card-overlay { - backdrop-filter: blur(12px); - -webkit-backdrop-filter: blur(12px); - border: 1px solid rgba(255, 255, 255, 0.2); + background: white; + border: 1px solid #e5e7eb; + box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05); } -/* Gradient Overlay Enhancement */ +/* Refined Gradient Overlay */ .gradient-overlay { background: linear-gradient(to top, rgba(0, 0, 0, 0.7) 0%, - rgba(0, 0, 0, 0.3) 40%, - transparent 70% + rgba(0, 0, 0, 0.4) 30%, + rgba(0, 0, 0, 0.1) 60%, + transparent 80% ); + border-radius: 0 0 12px 12px; } -/* Animation for new cards */ +/* Enhanced Animation for new cards */ @keyframes slideInFromRight { from { opacity: 0; - transform: translateX(20px); + transform: translateX(20px) scale(0.95); + } + 50% { + transform: translateX(-2px) scale(1.02); } to { opacity: 1; - transform: translateX(0); + transform: translateX(0) scale(1); } } .new-linked-card { - animation: slideInFromRight 0.3s ease-out; + animation: slideInFromRight 0.4s cubic-bezier(0.4, 0, 0.2, 1); } -/* Card Hover State Enhancements */ +/* Enhanced Card Hover States */ .linked-content-card:hover .name-card-overlay { background: rgba(255, 255, 255, 0.98); transform: translateY(-2px); + box-shadow: + 0 4px 6px -1px rgba(0, 0, 0, 0.1), + 0 2px 4px -1px rgba(0, 0, 0, 0.06); } .linked-content-card:hover .type-badge { - transform: scale(1.05); + transform: scale(1.05) translateY(-1px); +} + +/* Paper texture for linked content */ +.linked-content-card::after { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-image: + linear-gradient(45deg, transparent 48%, rgba(255,255,255,0.05) 49%, rgba(255,255,255,0.05) 51%, transparent 52%), + linear-gradient(-45deg, transparent 48%, rgba(255,255,255,0.03) 49%, rgba(255,255,255,0.03) 51%, transparent 52%); + background-size: 2px 2px; + pointer-events: none; + opacity: 0; + transition: opacity 0.3s ease; +} + +.linked-content-card:hover::after { + opacity: 1; } /* Text Selection Prevention */ @@ -172,10 +247,15 @@ } } -/* Focus states for accessibility */ +/* Enhanced Focus states for accessibility */ .linked-content-card:focus-within { outline: 2px solid #10B981; outline-offset: 2px; + transform: translateY(-2px); + box-shadow: + 0 0 0 4px rgba(16, 185, 129, 0.1), + 0 10px 25px -3px rgba(0, 0, 0, 0.1), + 0 4px 6px -2px rgba(0, 0, 0, 0.05); } .linked-content-card a:focus { diff --git a/app/assets/stylesheets/timeline_sidebar.css b/app/assets/stylesheets/timeline_sidebar.css new file mode 100644 index 00000000..4df61a87 --- /dev/null +++ b/app/assets/stylesheets/timeline_sidebar.css @@ -0,0 +1,377 @@ +/* ========================= + TIMELINE SIDEBAR STYLING + Modern Tool Panel Design + ========================= */ + +/* Sidebar Container */ +.timeline-sidebar { + background: linear-gradient(135deg, #ffffff 0%, #fefefe 100%); + border-right: 1px solid #e5e7eb; + box-shadow: + inset -1px 0 0 0 rgba(255, 255, 255, 0.5), + 2px 0 8px -2px rgba(0, 0, 0, 0.05); + position: relative; + overflow: hidden; +} + +.timeline-sidebar::before { + content: ''; + position: absolute; + top: 0; + right: 0; + bottom: 0; + width: 1px; + background: linear-gradient(180deg, + transparent 0%, + rgba(16, 185, 129, 0.1) 50%, + transparent 100% + ); +} + +/* Sidebar Header */ +.timeline-sidebar .px-6.py-4.border-b { + background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%); + border-bottom: 1px solid #e2e8f0; + position: relative; +} + +.timeline-sidebar .px-6.py-4.border-b::after { + content: ''; + position: absolute; + bottom: 0; + left: 0; + right: 0; + height: 1px; + background: linear-gradient(90deg, + transparent 0%, + #cbd5e1 50%, + transparent 100% + ); +} + +/* Section Styling */ +.timeline-sidebar .p-6.border-b { + border-bottom: 1px solid #f1f5f9; + position: relative; + transition: all 0.2s ease; +} + +.timeline-sidebar .p-6.border-b::after { + content: ''; + position: absolute; + bottom: 0; + left: 1.5rem; + right: 1.5rem; + height: 1px; + background: linear-gradient(90deg, + transparent 0%, + #e2e8f0 50%, + transparent 100% + ); +} + +.timeline-sidebar .p-6.border-b:hover { + background: rgba(248, 250, 252, 0.5); +} + +/* Search Input Styling */ +.timeline-sidebar input[type="text"] { + background: white; + border: 1px solid #e5e7eb; + border-radius: 12px; + padding: 0.75rem 1rem 0.75rem 2.5rem; + font-size: 0.875rem; + transition: all 0.2s ease; + box-shadow: + 0 1px 2px 0 rgba(0, 0, 0, 0.05), + inset 0 1px 0 0 rgba(255, 255, 255, 0.05); +} + +.timeline-sidebar input[type="text"]:hover { + background: #fefefe; + border-color: #d1d5db; + box-shadow: + 0 1px 3px 0 rgba(0, 0, 0, 0.1), + 0 1px 2px 0 rgba(0, 0, 0, 0.06); +} + +.timeline-sidebar input[type="text"]:focus { + background: white; + border-color: #10b981; + box-shadow: + 0 0 0 3px rgba(16, 185, 129, 0.1), + 0 1px 3px 0 rgba(0, 0, 0, 0.1), + 0 1px 2px 0 rgba(0, 0, 0, 0.06); + outline: none; +} + +/* Search Icon Styling */ +.timeline-sidebar .absolute.inset-y-0.left-0 { + display: flex; + align-items: center; + justify-content: center; + width: 2.5rem; + color: #9ca3af; + transition: color 0.2s ease; +} + +.timeline-sidebar input[type="text"]:focus + .absolute.inset-y-0.left-0, +.timeline-sidebar input[type="text"]:hover + .absolute.inset-y-0.left-0 { + color: #10b981; +} + +/* Quick Action Buttons */ +.timeline-sidebar button { + transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); + position: relative; + overflow: hidden; +} + +.timeline-sidebar button::before { + content: ''; + position: absolute; + top: 0; + left: -100%; + width: 100%; + height: 100%; + background: linear-gradient(90deg, + transparent 0%, + rgba(255, 255, 255, 0.2) 50%, + transparent 100% + ); + transition: left 0.5s ease; +} + +.timeline-sidebar button:hover::before { + left: 100%; +} + +/* Primary Button (Add Event) */ +.timeline-sidebar .bg-green-600 { + background: linear-gradient(135deg, #10b981 0%, #059669 100%); + border: 1px solid #059669; + box-shadow: + 0 2px 4px 0 rgba(16, 185, 129, 0.2), + 0 1px 2px 0 rgba(0, 0, 0, 0.06); +} + +.timeline-sidebar .bg-green-600:hover { + background: linear-gradient(135deg, #059669 0%, #047857 100%); + transform: translateY(-1px); + box-shadow: + 0 4px 8px 0 rgba(16, 185, 129, 0.3), + 0 2px 4px 0 rgba(0, 0, 0, 0.1); +} + +.timeline-sidebar .bg-green-600:active { + transform: translateY(0); + box-shadow: + 0 1px 2px 0 rgba(16, 185, 129, 0.2), + 0 1px 1px 0 rgba(0, 0, 0, 0.06); +} + +/* Secondary Button (Import Events) */ +.timeline-sidebar .bg-white.border { + background: white; + border: 1px solid #e5e7eb; + box-shadow: + 0 1px 2px 0 rgba(0, 0, 0, 0.05), + inset 0 1px 0 0 rgba(255, 255, 255, 0.1); +} + +.timeline-sidebar .bg-white.border:hover { + background: #f9fafb; + border-color: #d1d5db; + transform: translateY(-1px); + box-shadow: + 0 2px 4px 0 rgba(0, 0, 0, 0.1), + 0 1px 2px 0 rgba(0, 0, 0, 0.06); +} + +.timeline-sidebar .bg-white.border:active { + transform: translateY(0); + background: #f3f4f6; +} + +/* Stats Section Styling */ +.timeline-sidebar .space-y-3 { + padding: 0.5rem 0; +} + +.timeline-sidebar .flex.justify-between.items-center { + padding: 0.5rem 0; + border-radius: 8px; + transition: all 0.15s ease; + position: relative; +} + +.timeline-sidebar .flex.justify-between.items-center:hover { + background: rgba(16, 185, 129, 0.05); + transform: translateX(4px); +} + +.timeline-sidebar .flex.justify-between.items-center::before { + content: ''; + position: absolute; + left: -0.5rem; + top: 0; + bottom: 0; + width: 3px; + background: #10b981; + border-radius: 0 2px 2px 0; + opacity: 0; + transition: opacity 0.15s ease; +} + +.timeline-sidebar .flex.justify-between.items-center:hover::before { + opacity: 1; +} + +/* Stats Text Styling */ +.timeline-sidebar .text-sm.text-gray-600 { + color: #6b7280; + font-weight: 500; + transition: color 0.15s ease; +} + +.timeline-sidebar .text-sm.font-medium.text-gray-900 { + color: #1f2937; + font-weight: 600; + background: linear-gradient(135deg, #10b981 0%, #059669 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + transition: all 0.15s ease; +} + +/* Section Headers */ +.timeline-sidebar h3, +.timeline-sidebar h4 { + color: #1f2937; + font-weight: 600; + position: relative; +} + +.timeline-sidebar h3::after, +.timeline-sidebar h4::after { + content: ''; + position: absolute; + bottom: -0.25rem; + left: 0; + width: 2rem; + height: 2px; + background: linear-gradient(90deg, #10b981 0%, transparent 100%); + border-radius: 1px; +} + +/* Scrollbar Styling */ +.timeline-sidebar .overflow-y-auto::-webkit-scrollbar { + width: 6px; +} + +.timeline-sidebar .overflow-y-auto::-webkit-scrollbar-track { + background: transparent; +} + +.timeline-sidebar .overflow-y-auto::-webkit-scrollbar-thumb { + background: rgba(156, 163, 175, 0.3); + border-radius: 3px; +} + +.timeline-sidebar .overflow-y-auto::-webkit-scrollbar-thumb:hover { + background: rgba(156, 163, 175, 0.5); +} + +/* Mobile Responsive */ +@media (max-width: 1024px) { + .timeline-sidebar { + position: fixed; + top: 0; + left: -100%; + width: 20rem; + height: 100vh; + z-index: 50; + transition: left 0.3s ease; + box-shadow: + 2px 0 10px 0 rgba(0, 0, 0, 0.1), + 0 0 0 1px rgba(0, 0, 0, 0.05); + } + + .timeline-sidebar.open { + left: 0; + } + + /* Mobile backdrop */ + .timeline-sidebar-backdrop { + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.5); + z-index: 40; + opacity: 0; + pointer-events: none; + transition: opacity 0.3s ease; + } + + .timeline-sidebar-backdrop.open { + opacity: 1; + pointer-events: auto; + } +} + +/* Focus and Accessibility */ +.timeline-sidebar button:focus { + outline: 2px solid #10b981; + outline-offset: 2px; +} + +.timeline-sidebar input:focus { + outline: none; +} + +/* Loading States */ +.timeline-sidebar button:disabled { + opacity: 0.6; + cursor: not-allowed; + transform: none !important; +} + +.timeline-sidebar button:disabled:hover { + transform: none !important; + box-shadow: none !important; +} + +/* Animation for sidebar reveal */ +@keyframes slideInFromLeft { + from { + transform: translateX(-100%); + opacity: 0; + } + to { + transform: translateX(0); + opacity: 1; + } +} + +.timeline-sidebar.animate-in { + animation: slideInFromLeft 0.3s ease-out; +} + +/* Subtle pattern overlay */ +.timeline-sidebar::after { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-image: + radial-gradient(circle at 1px 1px, rgba(16, 185, 129, 0.02) 1px, transparent 0); + background-size: 20px 20px; + pointer-events: none; + z-index: 1; +} + +.timeline-sidebar > * { + position: relative; + z-index: 2; +} \ No newline at end of file diff --git a/app/controllers/documents_controller.rb b/app/controllers/documents_controller.rb index d1385bd8..ff37a608 100644 --- a/app/controllers/documents_controller.rb +++ b/app/controllers/documents_controller.rb @@ -298,8 +298,6 @@ class DocumentsController < ApplicationController def determine_layout if action_name == 'edit' 'editor' - elsif ['index', 'show'].include?(action_name) - 'tailwind' elsif action_name == 'plaintext' 'plaintext' else diff --git a/app/controllers/timeline_events_controller.rb b/app/controllers/timeline_events_controller.rb index 99c5cc35..b9c5e6ca 100644 --- a/app/controllers/timeline_events_controller.rb +++ b/app/controllers/timeline_events_controller.rb @@ -2,7 +2,7 @@ class TimelineEventsController < ApplicationController before_action :set_timeline_event, only: [ :show, :edit, :update, :destroy, :move_up, :move_to_top, :move_down, :move_to_bottom, - :link_entity, :unlink_entity + :link_entity, :unlink_entity, :add_tag, :remove_tag ] # GET /timeline_events @@ -39,11 +39,17 @@ class TimelineEventsController < ApplicationController # PATCH/PUT /timeline_events/1 def update if @timeline_event.update(timeline_event_params) - redirect_to @timeline_event, notice: 'Timeline event was successfully updated.' + respond_to do |format| + format.html { redirect_to @timeline_event, notice: 'Timeline event was successfully updated.' } + format.json { render json: { status: 'success', message: 'Timeline event updated successfully' } } + format.js { render json: { status: 'success', message: 'Timeline event updated successfully' } } + end else - require 'pry' - binding.pry - render json: :failure + respond_to do |format| + format.html { render :edit } + format.json { render json: { status: 'error', message: 'Failed to update timeline event', errors: @timeline_event.errors.full_messages }, status: :unprocessable_entity } + format.js { render json: { status: 'error', message: 'Failed to update timeline event', errors: @timeline_event.errors.full_messages }, status: :unprocessable_entity } + end end end @@ -122,6 +128,67 @@ class TimelineEventsController < ApplicationController @timeline_event.move_to_bottom if @timeline_event.can_be_modified_by?(current_user) end + def add_tag + return render json: { error: 'Not authorized' }, status: :forbidden unless @timeline_event.can_be_modified_by?(current_user) + + tag_name = params[:tag_name]&.strip + return render json: { error: 'Tag name is required' }, status: :bad_request if tag_name.blank? + + # Check if tag already exists for this event + existing_tag = @timeline_event.page_tags.find_by(tag: tag_name) + if existing_tag + return render json: { + status: 'error', + message: 'Tag already exists for this event' + }, status: :unprocessable_entity + end + + # Create the tag + tag = @timeline_event.page_tags.create( + tag: tag_name, + slug: PageTagService.slug_for(tag_name), + user: current_user + ) + + if tag.persisted? + render json: { + status: 'success', + message: 'Tag added successfully', + tag: { + id: tag.id, + name: tag.tag + } + } + else + render json: { + status: 'error', + message: 'Failed to add tag', + errors: tag.errors.full_messages + }, status: :unprocessable_entity + end + end + + def remove_tag + return render json: { error: 'Not authorized' }, status: :forbidden unless @timeline_event.can_be_modified_by?(current_user) + + tag_name = params[:tag_name]&.strip + return render json: { error: 'Tag name is required' }, status: :bad_request if tag_name.blank? + + tag = @timeline_event.page_tags.find_by(tag: tag_name) + + if tag&.destroy + render json: { + status: 'success', + message: 'Tag removed successfully' + } + else + render json: { + status: 'error', + message: 'Tag not found or failed to remove' + }, status: :unprocessable_entity + end + end + private # Use callbacks to share common setup or constraints between actions. diff --git a/app/controllers/timelines_controller.rb b/app/controllers/timelines_controller.rb index 4ceaf72f..fadcb124 100644 --- a/app/controllers/timelines_controller.rb +++ b/app/controllers/timelines_controller.rb @@ -77,6 +77,9 @@ class TimelinesController < ApplicationController @suggested_page_tags = [] cache_linkable_content_for_each_content_type + # Collect all unique tags from timeline events for filtering + @timeline_event_tags = collect_timeline_event_tags + # Get content already linked to other events in this timeline @timeline_linked_content = {} @timeline_content_summary = {} @@ -192,4 +195,16 @@ class TimelinesController < ApplicationController def set_sidenav_expansion @sidenav_expansion = 'writing' end + + def collect_timeline_event_tags + # Get all unique tags from timeline events with their usage counts + tag_counts = @timeline.timeline_events + .joins(:page_tags) + .group('page_tags.tag') + .count + + # Return array of hashes with tag name and count for easy access in view + tag_counts.map { |tag, count| { name: tag, count: count } } + .sort_by { |tag_data| tag_data[:name].downcase } + end end diff --git a/app/models/timelines/timeline_event.rb b/app/models/timelines/timeline_event.rb index 18be3c9c..8babade3 100644 --- a/app/models/timelines/timeline_event.rb +++ b/app/models/timelines/timeline_event.rb @@ -4,59 +4,42 @@ class TimelineEvent < ApplicationRecord belongs_to :timeline, touch: true has_many :timeline_event_entities, dependent: :destroy + + include HasPageTags acts_as_list scope: [:timeline_id] - # Validations - validates :event_type, presence: true, inclusion: { in: %w[general plot_event character_development world_event historical battle political cultural mystery research] } - validates :importance_level, presence: true, inclusion: { in: %w[major minor background] } - validates :status, presence: true, inclusion: { in: %w[planned happening completed cancelled] } - - # Scopes - scope :by_type, ->(type) { where(event_type: type) } - scope :by_importance, ->(level) { where(importance_level: level) } - scope :by_status, ->(status) { where(status: status) } - scope :major_events, -> { where(importance_level: 'major') } - scope :completed_events, -> { where(status: 'completed') } - - # Event type definitions with display info + # Event type definitions with narrative focus and Material Icons EVENT_TYPES = { - 'general' => { name: 'General Event', color: 'gray', icon: 'event' }, - 'plot_event' => { name: 'Plot Event', color: 'blue', icon: 'timeline' }, - 'character_development' => { name: 'Character Development', color: 'green', icon: 'person' }, - 'world_event' => { name: 'World Event', color: 'purple', icon: 'public' }, - 'historical' => { name: 'Historical', color: 'amber', icon: 'history' }, - 'battle' => { name: 'Battle/Conflict', color: 'red', icon: 'sword' }, - 'political' => { name: 'Political', color: 'indigo', icon: 'gavel' }, - 'cultural' => { name: 'Cultural', color: 'pink', icon: 'celebration' }, - 'mystery' => { name: 'Mystery/Clue', color: 'yellow', icon: 'search' }, - 'research' => { name: 'Research Note', color: 'teal', icon: 'book' } + 'general' => { name: 'General', icon: 'radio_button_checked' }, + 'setup' => { name: 'Setup', icon: 'foundation' }, + 'exposition' => { name: 'Exposition', icon: 'info' }, + 'inciting_incident' => { name: 'Inciting Incident', icon: 'flash_on' }, + 'complication' => { name: 'Complication', icon: 'warning' }, + 'obstacle' => { name: 'Obstacle', icon: 'block' }, + 'conflict' => { name: 'Conflict', icon: 'gavel' }, + 'progress' => { name: 'Progress', icon: 'trending_up' }, + 'revelation' => { name: 'Revelation', icon: 'visibility' }, + 'transformation' => { name: 'Transformation', icon: 'autorenew' }, + 'climax' => { name: 'Climax', icon: 'whatshot' }, + 'resolution' => { name: 'Resolution', icon: 'check_circle' }, + 'aftermath' => { name: 'Aftermath', icon: 'restore' } }.freeze - IMPORTANCE_LEVELS = { - 'major' => { name: 'Major Event', weight: 3 }, - 'minor' => { name: 'Minor Event', weight: 2 }, - 'background' => { name: 'Background Detail', weight: 1 } - }.freeze - - STATUS_OPTIONS = { - 'planned' => { name: 'Planned', color: 'yellow' }, - 'happening' => { name: 'Happening', color: 'blue' }, - 'completed' => { name: 'Completed', color: 'green' }, - 'cancelled' => { name: 'Cancelled', color: 'gray' } - }.freeze + # Validation + validates :event_type, inclusion: { in: EVENT_TYPES.keys } # Helper methods def event_type_info EVENT_TYPES[event_type] || EVENT_TYPES['general'] end - def importance_info - IMPORTANCE_LEVELS[importance_level] || IMPORTANCE_LEVELS['minor'] + def event_type_icon + event_type_info[:icon] end - def status_info - STATUS_OPTIONS[status] || STATUS_OPTIONS['completed'] + def event_type_name + event_type_info[:name] end def has_duration? @@ -68,14 +51,6 @@ class TimelineEvent < ApplicationRecord "#{time_label} - #{end_time_label}" end - def color_class - "timeline-event-#{event_type_info[:color]}" - end - - def importance_weight - importance_info[:weight] - end - # todo move this to a real permissions authorizer def can_be_modified_by?(user) user == timeline.user diff --git a/app/views/shared/_timeline_event_linked_content.html.erb b/app/views/shared/_timeline_event_linked_content.html.erb index dfa7796b..652e17be 100644 --- a/app/views/shared/_timeline_event_linked_content.html.erb +++ b/app/views/shared/_timeline_event_linked_content.html.erb @@ -1,12 +1,12 @@ <% if timeline_event.timeline_event_entities.any? %>
+ Show only events with selected tags + Show all events, highlight selected tags +
+No tags found
+Add tags to events to enable filtering
+