diff --git a/app/assets/stylesheets/application.tailwind.css b/app/assets/stylesheets/application.tailwind.css index f1d8c73..b260630 100644 --- a/app/assets/stylesheets/application.tailwind.css +++ b/app/assets/stylesheets/application.tailwind.css @@ -1 +1,93 @@ @import "tailwindcss"; + +@layer components { + [data-stream-view-mode="table"] .streams-table { + display: table !important; + } + + [data-stream-view-mode="table"] .streams-thead { + display: table-header-group !important; + } + + [data-stream-view-mode="table"] .streams-container { + overflow-x: auto !important; + } + + [data-stream-view-mode="table"] .streams-card-sort { + display: none !important; + } + + [data-stream-view-mode="table"] .streams-tbody { + display: table-row-group !important; + } + + [data-stream-view-mode="table"] .streams-tbody > :not([hidden]) ~ :not([hidden]) { + margin-top: 0 !important; + } + + [data-stream-view-mode="table"] .streams-row { + display: table-row !important; + border: 0 !important; + border-radius: 0 !important; + box-shadow: none !important; + overflow: visible !important; + background-color: transparent !important; + } + + [data-stream-view-mode="table"] .streams-row:hover { + background-color: rgb(249 250 251) !important; + } + + [data-stream-view-mode="table"] .streams-cell { + display: table-cell !important; + } + + [data-stream-view-mode="table"] .streams-cell::before { + display: none !important; + } + + [data-stream-view-mode="card"] .streams-table { + display: block !important; + } + + [data-stream-view-mode="card"] .streams-thead { + display: none !important; + } + + [data-stream-view-mode="card"] .streams-container { + overflow: visible !important; + } + + [data-stream-view-mode="card"] .streams-card-sort { + display: flex !important; + } + + [data-stream-view-mode="card"] .streams-tbody { + display: block !important; + } + + [data-stream-view-mode="card"] .streams-tbody > :not([hidden]) ~ :not([hidden]) { + margin-top: 1rem !important; + } + + [data-stream-view-mode="card"] .streams-row { + display: block !important; + border: 1px solid rgb(229 231 235) !important; + border-radius: 0.5rem !important; + box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05) !important; + overflow: hidden !important; + background-color: white !important; + } + + [data-stream-view-mode="card"] .streams-row:hover { + background-color: rgb(249 250 251) !important; + } + + [data-stream-view-mode="card"] .streams-cell { + display: block !important; + } + + [data-stream-view-mode="card"] .streams-cell::before { + display: block !important; + } +} diff --git a/app/javascript/controllers/index.js b/app/javascript/controllers/index.js index 4696740..eab401e 100644 --- a/app/javascript/controllers/index.js +++ b/app/javascript/controllers/index.js @@ -6,6 +6,7 @@ import SearchController from './search_controller' import MobileMenuController from './mobile_menu_controller' import CollaborativeSpreadsheetController from './collaborative_spreadsheet_controller' import StreamTablePreferencesController from './stream_table_preferences_controller' +import StreamViewController from './stream_view_controller' import ToastController from './toast_controller' // Register controllers @@ -14,4 +15,5 @@ application.register('search', SearchController) application.register('mobile-menu', MobileMenuController) application.register('collaborative-spreadsheet', CollaborativeSpreadsheetController) application.register('stream-table-preferences', StreamTablePreferencesController) +application.register('stream-view', StreamViewController) application.register('toast', ToastController) diff --git a/app/javascript/controllers/stream_view_controller.js b/app/javascript/controllers/stream_view_controller.js new file mode 100644 index 0000000..52dca85 --- /dev/null +++ b/app/javascript/controllers/stream_view_controller.js @@ -0,0 +1,115 @@ +import { Controller } from '@hotwired/stimulus' + +export default class extends Controller { + static targets = ['toggle'] + static values = { + storageKey: { type: String, default: 'streamsViewMode' } + } + + connect () { + this.boundHandleMediaChange = this.handleMediaChange.bind(this) + this.mediaQuery = window.matchMedia('(min-width: 768px)') + + this.applySavedOrDefault() + this.addMediaListener() + } + + disconnect () { + this.removeMediaListener() + } + + select (event) { + const mode = event.currentTarget.dataset.mode + if (!this.isValidMode(mode)) return + + this.storeMode(mode) + this.applyMode(mode) + } + + handleMediaChange () { + this.applySavedOrDefault() + } + + defaultMode () { + return this.mediaQuery.matches ? 'table' : 'card' + } + + applySavedOrDefault () { + const savedMode = this.loadSavedMode() + if (savedMode) { + this.applyMode(savedMode) + return + } + + this.applyMode(this.defaultMode()) + } + + applyMode (mode) { + if (!this.isValidMode(mode)) return + + this.element.dataset.streamViewMode = mode + this.updateToggleStyles(mode) + } + + updateToggleStyles (mode) { + if (!this.hasToggleTarget) return + + this.toggleTargets.forEach(toggle => { + const active = toggle.dataset.mode === mode + toggle.setAttribute('aria-pressed', active ? 'true' : 'false') + toggle.classList.toggle('bg-indigo-600', active) + toggle.classList.toggle('text-white', active) + toggle.classList.toggle('hover:bg-indigo-700', active) + toggle.classList.toggle('bg-white', !active) + toggle.classList.toggle('text-gray-600', !active) + toggle.classList.toggle('hover:text-gray-900', !active) + toggle.classList.toggle('hover:bg-gray-50', !active) + }) + } + + isValidMode (mode) { + return mode === 'table' || mode === 'card' + } + + loadSavedMode () { + try { + const mode = window.localStorage.getItem(this.storageKey()) + return this.isValidMode(mode) ? mode : null + } catch { + return null + } + } + + storeMode (mode) { + try { + window.localStorage.setItem(this.storageKey(), mode) + } catch { + // Ignore storage errors (private mode, disabled storage, etc.) + } + } + + storageKey () { + const suffix = this.mediaQuery && this.mediaQuery.matches ? 'desktop' : 'mobile' + return `${this.storageKeyValue}-${suffix}` + } + + addMediaListener () { + if (!this.mediaQuery) return + + if (this.mediaQuery.addEventListener) { + this.mediaQuery.addEventListener('change', this.boundHandleMediaChange) + } else if (this.mediaQuery.addListener) { + this.mediaQuery.addListener(this.boundHandleMediaChange) + } + } + + removeMediaListener () { + if (!this.mediaQuery) return + + if (this.mediaQuery.removeEventListener) { + this.mediaQuery.removeEventListener('change', this.boundHandleMediaChange) + } else if (this.mediaQuery.removeListener) { + this.mediaQuery.removeListener(this.boundHandleMediaChange) + } + } +} diff --git a/app/views/admin/streams/_spreadsheet.html.erb b/app/views/admin/streams/_spreadsheet.html.erb index 9c94e3a..e242dfa 100644 --- a/app/views/admin/streams/_spreadsheet.html.erb +++ b/app/views/admin/streams/_spreadsheet.html.erb @@ -1,50 +1,83 @@ -
| " @@ -166,7 +228,7 @@ |
|---|