mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-18 21:16:47 +00:00
feat(collections): say what a place is without having to click it
Asked for in the feedback: a wall of round photos, and no way to tell which is which. "Name and displaying any custom rating would be very beneficial to quickly seeing what is around you that is of interest." The collections map had hover switched off. The trip map has told you what a marker is all along, so it is the same card, now shared instead of written a third time — it already existed twice, once per renderer, and the two had drifted in how they resolved the category icon. The card gains a rating, and so does the marker itself: an amber disc on the corner where a trip's day numbers sit. The two never collide, because a numbered stop is one already planned into a day and this answers the question asked before that. Nothing is drawn for an unrated place, which is most of them. Also from this round: the place panel's labels move up beside the name, where a label belongs — read three rows down they looked like a property of the rating above them — and the rating gets a ground of its own so a line of stars stops reading as another row of text.
This commit is contained in:
@@ -29,7 +29,10 @@ export default function CollectionMap({ places, selectedPlaceId, onOpenPlace, on
|
||||
<MapViewAuto
|
||||
places={pts}
|
||||
selectedPlaceId={selectedPlaceId}
|
||||
hoverDisabled
|
||||
// Hover was off here, so a wall of round photos gave no way to tell what any of
|
||||
// them were without clicking. The trip map has said what a marker is all along;
|
||||
// this one adds the rating, because "which of these did I like" is exactly the
|
||||
// question a small picture cannot answer.
|
||||
onMarkerClick={onOpenPlace}
|
||||
onMapClick={onDeselect ? () => onDeselect() : undefined}
|
||||
// No center/zoom: the map frames itself on the collection's places at mount, and
|
||||
|
||||
@@ -216,7 +216,23 @@ export default function CollectionPlaceDetail({
|
||||
<div className="col-detail-head">
|
||||
{editing
|
||||
? <input value={name} onChange={e => setName(e.target.value)} className="col-detail-name-input" autoFocus aria-label={t('collections.listName')} />
|
||||
: <h2 className="col-detail-name">{place.name}</h2>}
|
||||
: (
|
||||
/* Beside the name, not three rows below it: a label is part of what this
|
||||
place IS, the same as its name, and read on its own further down it
|
||||
looked like a property of the rating above it. */
|
||||
<div className="col-detail-titlerow">
|
||||
<h2 className="col-detail-name">{place.name}</h2>
|
||||
{assignedLabels.length > 0 && (
|
||||
<div className="col-detail-labels on-cover">
|
||||
{assignedLabels.map(l => (
|
||||
<span key={l.id} className="col-labelchip on static" style={{ ['--label' as string]: l.color || 'var(--accent)' }}>
|
||||
<span className="col-labelchip-dot" /> {l.name}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -236,7 +252,9 @@ export default function CollectionPlaceDetail({
|
||||
|
||||
{/* Collaborative rating (#1435) — every member votes; the average shows. */}
|
||||
{onRate && (
|
||||
<div style={{ padding: '2px 0' }}>
|
||||
/* In a panel of full-width rows, a bare line of stars read as another row of
|
||||
text. Set on its own ground it reads as the one thing here you can answer. */
|
||||
<div className="col-detail-rating">
|
||||
<PlaceRating ratings={place.ratings ?? []} ratingAvg={place.rating_avg} onRate={onRate} />
|
||||
</div>
|
||||
)}
|
||||
@@ -311,15 +329,6 @@ export default function CollectionPlaceDetail({
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{assignedLabels.length > 0 && (
|
||||
<div className="col-detail-labels">
|
||||
{assignedLabels.map(l => (
|
||||
<span key={l.id} className="col-labelchip on static" style={{ ['--label' as string]: l.color || 'var(--accent)' }}>
|
||||
<span className="col-labelchip-dot" /> {l.name}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{place.description && (
|
||||
<div className="col-detail-md collab-note-md">
|
||||
<Markdown remarkPlugins={[remarkGfm, remarkBreaks]} components={markdownLinkComponents}>{place.description}</Markdown>
|
||||
|
||||
@@ -36,7 +36,9 @@ import L from 'leaflet'
|
||||
import 'leaflet.markercluster/dist/MarkerCluster.css'
|
||||
import 'leaflet.markercluster/dist/MarkerCluster.Default.css'
|
||||
import { mapsApi } from '../../api/client'
|
||||
import { getCategoryIcon, CATEGORY_ICON_MAP } from '../shared/categoryIcons'
|
||||
import { CATEGORY_ICON_MAP } from '../shared/categoryIcons'
|
||||
import PlaceHoverCard from './PlaceHoverCard'
|
||||
import { ratingBadgeHtml } from './ratingBadge'
|
||||
import ReservationOverlay from './ReservationOverlay'
|
||||
import { PluginMapMarkers } from './MapPluginMarkers'
|
||||
import { PluginMapLayers } from './MapPluginLayers'
|
||||
@@ -132,7 +134,7 @@ function RouteViaMarker({ via, controls, eventHandlers, children }: {
|
||||
* Shows image_url if available, otherwise category icon in colored circle.
|
||||
*/
|
||||
function createPlaceIcon(place, orderNumbers, isSelected) {
|
||||
const cacheKey = `${place.id}:${isSelected}:${place.image_url || ''}:${place.category_color || ''}:${place.category_icon || ''}:${place.stop_type || ''}:${orderNumbers?.join(',') || ''}`
|
||||
const cacheKey = `${place.id}:${isSelected}:${place.image_url || ''}:${place.category_color || ''}:${place.category_icon || ''}:${place.stop_type || ''}:${orderNumbers?.join(',') || ''}:${(place as { rating_avg?: number | null }).rating_avg ?? ''}`
|
||||
const cached = iconCache.get(cacheKey)
|
||||
if (cached) return cached
|
||||
|
||||
@@ -157,8 +159,10 @@ function createPlaceIcon(place, orderNumbers, isSelected) {
|
||||
: '0 2px 8px rgba(0,0,0,0.22)'
|
||||
const bgColor = safeHexColor(place.category_color, '#6b7280')
|
||||
|
||||
// Number badges (bottom-right)
|
||||
let badgeHtml = ''
|
||||
// Number badges (bottom-right), or the rating where there are none: a numbered stop
|
||||
// is one already planned into a day, and the rating answers the question asked before
|
||||
// that. The two never want the same corner at the same time.
|
||||
let badgeHtml = ratingBadgeHtml((place as { rating_avg?: number | null }).rating_avg)
|
||||
if (orderNumbers && orderNumbers.length > 0) {
|
||||
const label = orderNumbers.join(' · ')
|
||||
badgeHtml = `<span style="
|
||||
@@ -970,7 +974,6 @@ export const MapView = memo(function MapView({
|
||||
|
||||
const TooltipOverlay = !hoverDisabled && hoveredPlace && tooltipPos && !isTouchDevice
|
||||
&& (!hoveredPlace.routeVia || routeVias.includes(hoveredPlace.routeVia))
|
||||
const CatIcon = TooltipOverlay ? getCategoryIcon(hoveredPlace.category_icon) : null
|
||||
|
||||
const { position: userPosition, mode: trackingMode, error: trackingError, errorCode: trackingErrorCode, cycleMode: cycleTrackingMode } = useGeolocation()
|
||||
// Desktop browsers only get IP-based geolocation (city-level accuracy),
|
||||
@@ -1237,35 +1240,16 @@ export const MapView = memo(function MapView({
|
||||
<NightPauseTooltip label={hoveredPlace.name ?? ''} x={tooltipPos.x} y={tooltipPos.y} />
|
||||
)}
|
||||
{TooltipOverlay && !hoveredPlace.routeVia?.nightPause && (
|
||||
<div data-testid="tooltip" style={{
|
||||
position: 'fixed',
|
||||
left: tooltipPos.x + 14,
|
||||
top: tooltipPos.y - 10,
|
||||
zIndex: 9999,
|
||||
pointerEvents: 'none',
|
||||
background: 'white',
|
||||
borderRadius: 8,
|
||||
boxShadow: '0 2px 10px rgba(0,0,0,0.15)',
|
||||
padding: '6px 10px',
|
||||
fontFamily: "var(--font-system)",
|
||||
maxWidth: 220,
|
||||
whiteSpace: 'nowrap',
|
||||
}}>
|
||||
<div style={{ fontWeight: 600, fontSize: 12, color: '#111827', overflow: 'hidden', textOverflow: 'ellipsis' }}>
|
||||
{hoveredPlace.name}
|
||||
</div>
|
||||
{hoveredPlace.category_name && CatIcon && (
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 3, marginTop: 1 }}>
|
||||
<CatIcon size={10} style={{ color: hoveredPlace.category_color || '#6b7280', flexShrink: 0 }} />
|
||||
<span style={{ fontSize: 11, color: '#6b7280' }}>{hoveredPlace.category_name}</span>
|
||||
</div>
|
||||
)}
|
||||
{hoveredPlace.address && (
|
||||
<div style={{ fontSize: 11, color: '#9ca3af', marginTop: 2, overflow: 'hidden', textOverflow: 'ellipsis' }}>
|
||||
{hoveredPlace.address}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<PlaceHoverCard
|
||||
x={tooltipPos.x}
|
||||
y={tooltipPos.y}
|
||||
name={hoveredPlace.name}
|
||||
categoryName={hoveredPlace.category_name}
|
||||
categoryIcon={hoveredPlace.category_icon}
|
||||
categoryColor={hoveredPlace.category_color}
|
||||
address={hoveredPlace.address}
|
||||
rating={hoveredPlace.rating_avg}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -33,6 +33,8 @@ import { useHazardLayerGL } from './useHazardLayerGL'
|
||||
import { useDawarichTrailGL } from './useDawarichTrailGL'
|
||||
import { bindDayBoundaryDrag, type DayBoundaryControls } from './dayBoundaryDrag'
|
||||
import NightPauseTooltip from './NightPauseTooltip'
|
||||
import PlaceHoverCard from './PlaceHoverCard'
|
||||
import { ratingBadgeHtml } from './ratingBadge'
|
||||
import { POI_CATEGORY_BY_KEY, type Poi } from './poiCategories'
|
||||
import { resolveTrackColor, hasManualTrackColor } from './trackColors'
|
||||
import { buildPoiPopupHtml } from './placePopup'
|
||||
@@ -336,7 +338,9 @@ function createMarkerElement(place: Place & { category_color?: string; category_
|
||||
// bleed outside it and the route lines would appear slightly off.
|
||||
const outer = size + borderWidth * 2
|
||||
|
||||
let badgeHtml = ''
|
||||
// Same corner, same rule as the Leaflet map: numbers when the place is planned into a
|
||||
// day, the rating when it is not.
|
||||
let badgeHtml = ratingBadgeHtml((place as { rating_avg?: number | null }).rating_avg)
|
||||
if (orderNumbers && orderNumbers.length > 0) {
|
||||
const label = orderNumbers.join(' · ')
|
||||
badgeHtml = `<span style="
|
||||
@@ -2288,8 +2292,6 @@ export function MapViewGL({
|
||||
? 'calc(var(--bottom-nav-h, 84px) + 20px + var(--day-panel-h, 0px) + 12px)'
|
||||
: 'calc(var(--bottom-nav-h, 84px) + 12px)'
|
||||
|
||||
const HoverIcon = (hoverPlace?.category_icon && CATEGORY_ICON_MAP[hoverPlace.category_icon]) || CATEGORY_ICON_MAP['MapPin']
|
||||
|
||||
return (
|
||||
<div className="w-full h-full relative">
|
||||
<div ref={containerRef} className="w-full h-full" />
|
||||
@@ -2322,35 +2324,16 @@ export function MapViewGL({
|
||||
<NightPauseTooltip label={hoverPlace.name ?? ''} x={hoverPos.x} y={hoverPos.y} />
|
||||
)}
|
||||
{!hoverDisabled && hoverPlace && !hoverPlace.routeVia?.nightPause && hoverPos && !isMobile && (!hoverPlace.routeVia || routeVias.includes(hoverPlace.routeVia)) && (
|
||||
<div data-testid="tooltip" style={{
|
||||
position: 'fixed',
|
||||
left: hoverPos.x + 14,
|
||||
top: hoverPos.y - 10,
|
||||
zIndex: 9999,
|
||||
pointerEvents: 'none',
|
||||
background: 'white',
|
||||
borderRadius: 8,
|
||||
boxShadow: '0 2px 10px rgba(0,0,0,0.15)',
|
||||
padding: '6px 10px',
|
||||
fontFamily: 'var(--font-system)',
|
||||
maxWidth: 220,
|
||||
whiteSpace: 'nowrap',
|
||||
}}>
|
||||
<div style={{ fontWeight: 600, fontSize: 12, color: '#111827', overflow: 'hidden', textOverflow: 'ellipsis' }}>
|
||||
{hoverPlace.name}
|
||||
</div>
|
||||
{hoverPlace.category_name && (
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 3, marginTop: 1 }}>
|
||||
<HoverIcon size={10} style={{ color: hoverPlace.category_color || '#6b7280', flexShrink: 0 }} />
|
||||
<span style={{ fontSize: 11, color: '#6b7280' }}>{hoverPlace.category_name}</span>
|
||||
</div>
|
||||
)}
|
||||
{hoverPlace.address && (
|
||||
<div style={{ fontSize: 11, color: '#9ca3af', marginTop: 2, overflow: 'hidden', textOverflow: 'ellipsis' }}>
|
||||
{hoverPlace.address}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<PlaceHoverCard
|
||||
x={hoverPos.x}
|
||||
y={hoverPos.y}
|
||||
name={hoverPlace.name}
|
||||
categoryName={hoverPlace.category_name}
|
||||
categoryIcon={hoverPlace.category_icon}
|
||||
categoryColor={hoverPlace.category_color}
|
||||
address={hoverPlace.address}
|
||||
rating={hoverPlace.rating_avg}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import React from 'react'
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import PlaceHoverCard from './PlaceHoverCard'
|
||||
|
||||
describe('PlaceHoverCard', () => {
|
||||
it('FE-MAP-HOVERCARD-001: says what the place is, which a round photo cannot', () => {
|
||||
render(<PlaceHoverCard x={10} y={20} name="Museumsinsel" categoryName="Sights" categoryIcon="Landmark" address="Bodestraße 1" />)
|
||||
|
||||
expect(screen.getByText('Museumsinsel')).toBeInTheDocument()
|
||||
expect(screen.getByText('Sights')).toBeInTheDocument()
|
||||
expect(screen.getByText('Bodestraße 1')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('FE-MAP-HOVERCARD-002: shows a rating beside the name, and only when there is one', () => {
|
||||
const { rerender } = render(<PlaceHoverCard x={0} y={0} name="Museumsinsel" rating={4} />)
|
||||
expect(screen.getByText('4')).toBeInTheDocument()
|
||||
|
||||
rerender(<PlaceHoverCard x={0} y={0} name="Museumsinsel" rating={null} />)
|
||||
expect(screen.queryByText('4')).toBeNull()
|
||||
})
|
||||
|
||||
it('FE-MAP-HOVERCARD-003: never takes the pointer — it is a label, not a target', () => {
|
||||
render(<PlaceHoverCard x={0} y={0} name="Museumsinsel" />)
|
||||
expect(screen.getByTestId('tooltip')).toHaveStyle({ pointerEvents: 'none' })
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,71 @@
|
||||
import React from 'react'
|
||||
import { Star } from 'lucide-react'
|
||||
import { getCategoryIcon } from '../shared/categoryIcons'
|
||||
|
||||
/**
|
||||
* What a place is, shown at the cursor while the pointer rests on its marker.
|
||||
*
|
||||
* One card for every map. It was written twice, once in each renderer, and the two
|
||||
* had already drifted in how they resolved the category icon; a third copy for the
|
||||
* collections map was not going to end better. Collections is also why it carries a
|
||||
* rating: on a wall of round photos, "which of these did I like" is the question the
|
||||
* picture cannot answer.
|
||||
*
|
||||
* Follows the cursor rather than anchoring to the marker, and never takes the
|
||||
* pointer: it is a label on the map, not something to aim at.
|
||||
*/
|
||||
export interface PlaceHoverCardProps {
|
||||
x: number
|
||||
y: number
|
||||
name: string | null | undefined
|
||||
categoryName?: string | null
|
||||
categoryIcon?: string | null
|
||||
categoryColor?: string | null
|
||||
address?: string | null
|
||||
/** Average across everyone who rated it, when the surface tracks ratings. */
|
||||
rating?: number | null
|
||||
}
|
||||
|
||||
export default function PlaceHoverCard({ x, y, name, categoryName, categoryIcon, categoryColor, address, rating }: PlaceHoverCardProps): React.ReactElement {
|
||||
const CatIcon = categoryName ? getCategoryIcon(categoryIcon) : null
|
||||
return (
|
||||
<div data-testid="tooltip" style={{
|
||||
position: 'fixed',
|
||||
left: x + 14,
|
||||
top: y - 10,
|
||||
zIndex: 9999,
|
||||
pointerEvents: 'none',
|
||||
background: 'white',
|
||||
borderRadius: 8,
|
||||
boxShadow: '0 2px 10px rgba(0,0,0,0.15)',
|
||||
padding: '6px 10px',
|
||||
fontFamily: 'var(--font-system)',
|
||||
maxWidth: 220,
|
||||
whiteSpace: 'nowrap',
|
||||
}}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
||||
<div style={{ fontWeight: 600, fontSize: 12, color: '#111827', overflow: 'hidden', textOverflow: 'ellipsis' }}>
|
||||
{name}
|
||||
</div>
|
||||
{typeof rating === 'number' && rating > 0 && (
|
||||
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 2, flexShrink: 0, fontSize: 11, fontWeight: 600, color: '#111827' }}>
|
||||
<Star size={10} style={{ fill: '#f59e0b', color: '#f59e0b' }} aria-hidden />
|
||||
{/* One decimal only when it earns it: "4" reads faster than "4.0". */}
|
||||
{Number.isInteger(rating) ? rating : rating.toFixed(1)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{categoryName && CatIcon && (
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 3, marginTop: 1 }}>
|
||||
<CatIcon size={10} style={{ color: categoryColor || '#6b7280', flexShrink: 0 }} />
|
||||
<span style={{ fontSize: 11, color: '#6b7280' }}>{categoryName}</span>
|
||||
</div>
|
||||
)}
|
||||
{address && (
|
||||
<div style={{ fontSize: 11, color: '#9ca3af', marginTop: 2, overflow: 'hidden', textOverflow: 'ellipsis' }}>
|
||||
{address}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -6,5 +6,8 @@ export interface MapHoverInfo {
|
||||
category_name?: string | null
|
||||
category_icon?: string | null
|
||||
category_color?: string | null
|
||||
/** Everyone's average, where the surface keeps ratings. On the collections map it
|
||||
* is the one thing a round photo cannot tell you: which of these you liked. */
|
||||
rating_avg?: number | null
|
||||
routeVia?: RouteVia
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { ratingBadgeHtml } from './ratingBadge'
|
||||
|
||||
describe('ratingBadgeHtml', () => {
|
||||
it('FE-MAP-RATINGBADGE-001: shows a whole rating without a decimal', () => {
|
||||
// "4" reads faster than "4.0", and at this size every character costs.
|
||||
expect(ratingBadgeHtml(4)).toContain('>4<')
|
||||
expect(ratingBadgeHtml(4)).not.toContain('4.0')
|
||||
})
|
||||
|
||||
it('FE-MAP-RATINGBADGE-002: keeps one decimal where an average has one', () => {
|
||||
expect(ratingBadgeHtml(4.5)).toContain('>4.5<')
|
||||
expect(ratingBadgeHtml(4.25)).toContain('>4.3<')
|
||||
})
|
||||
|
||||
it('FE-MAP-RATINGBADGE-003: draws nothing without a rating', () => {
|
||||
// An unrated place is most of them. A badge on every marker would say nothing and
|
||||
// cover the photo it sits on.
|
||||
expect(ratingBadgeHtml(null)).toBe('')
|
||||
expect(ratingBadgeHtml(undefined)).toBe('')
|
||||
expect(ratingBadgeHtml(0)).toBe('')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* The rating a place carries, as a disc on the corner of its marker.
|
||||
*
|
||||
* Bottom right, where a trip's day numbers sit — the two never appear together, since
|
||||
* a numbered stop is one you have already planned and this answers the question you
|
||||
* ask before that: of all these photos, which did I like. Amber rather than the number
|
||||
* badge's white, so a glance tells them apart without reading either.
|
||||
*
|
||||
* A rounded value and no star: at eighteen pixels a star is a smudge, and "4.5" is
|
||||
* already the whole sentence.
|
||||
*/
|
||||
export function ratingBadgeHtml(rating: number | null | undefined): string {
|
||||
if (typeof rating !== 'number' || !(rating > 0)) return '';
|
||||
const label = Number.isInteger(rating) ? String(rating) : rating.toFixed(1);
|
||||
return `<span style="
|
||||
position:absolute;bottom:-4px;right:-4px;
|
||||
min-width:18px;height:18px;border-radius:9px;
|
||||
padding:0 4px;
|
||||
background:#f59e0b;
|
||||
border:1.5px solid rgba(255,255,255,0.92);
|
||||
box-shadow:0 1px 4px rgba(0,0,0,0.25);
|
||||
display:flex;align-items:center;justify-content:center;
|
||||
font-size:9px;font-weight:800;color:#fff;
|
||||
font-family:var(--font-system);line-height:1;
|
||||
box-sizing:border-box;white-space:nowrap;
|
||||
">${label}</span>`;
|
||||
}
|
||||
@@ -318,6 +318,7 @@
|
||||
.trek-dash .col-labelchip.on { color: var(--label, var(--accent)); background: color-mix(in oklch, var(--label, var(--accent)) 16%, transparent); border-color: color-mix(in oklch, var(--label, var(--accent)) 42%, transparent); }
|
||||
.trek-dash .col-labelchip.on .col-labelchip-dot { background: var(--label, var(--accent)); }
|
||||
.trek-dash .col-labelchip.static { cursor: default; color: var(--label, var(--accent)); background: color-mix(in oklch, var(--label, var(--accent)) 14%, transparent); border-color: transparent; }
|
||||
.trek-dash .col-detail-rating { display: inline-flex; align-items: center; align-self: flex-start; gap: 8px; padding: 6px 12px; margin: 2px 0 4px; border-radius: 999px; background: var(--surface-2); border: 1px solid var(--line); }
|
||||
.trek-dash .col-detail-labels { display: flex; flex-wrap: wrap; gap: 7px; margin: 2px 0 10px; }
|
||||
|
||||
/* ----------------- filter bar (status + category dropdowns) ----------------- */
|
||||
@@ -433,6 +434,12 @@
|
||||
}
|
||||
.trek-dash .col-detail-close:hover { background: rgba(0,0,0,.6); }
|
||||
.trek-dash .col-detail-head { position: absolute; left: 20px; right: 20px; bottom: 12px; z-index: 1; color: #fff; }
|
||||
/* Name and labels on one line, wrapping together. The labels sit on the cover photo
|
||||
now, so they carry the same shadow the name does or they vanish into a bright sky. */
|
||||
.trek-dash .col-detail-titlerow { display: flex; align-items: baseline; flex-wrap: wrap; gap: 8px; min-width: 0; }
|
||||
.trek-dash .col-detail-titlerow .col-detail-name { flex: 0 1 auto; }
|
||||
.trek-dash .col-detail-labels.on-cover { margin: 0; flex: 0 1 auto; }
|
||||
.trek-dash .col-detail-labels.on-cover .col-labelchip { box-shadow: 0 1px 6px rgba(0,0,0,.45); }
|
||||
.trek-dash .col-detail-name { font-size: 22px; font-weight: 700; letter-spacing: -.02em; line-height: 1.1; margin: 0; text-shadow: 0 1px 6px rgba(0,0,0,.55); overflow: hidden; text-overflow: ellipsis; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; }
|
||||
.trek-dash .col-detail-name-input { width: 100%; font-size: 20px; font-weight: 700; background: rgba(255,255,255,.18); border: 1px solid rgba(255,255,255,.32); border-radius: 8px; padding: 3px 8px; color: #fff; outline: none; }
|
||||
.trek-dash .col-detail-body { padding: 16px 20px; overflow-y: auto; display: flex; flex-direction: column; gap: 16px; }
|
||||
|
||||
Reference in New Issue
Block a user