Commit 537931dc authored by shrabbit's avatar shrabbit
Browse files

feat: 新增地图实现章节(第四章)

通过 Gitea MCP 拉取 shrabbit/SeekMyGenshin 仓库 Issue #32 的 44 条评论,整理为 wiki/map.md 与 issue-32-raw.md,并开发 Nuxt 页面 /map。

页面结构(8 章节):

1. PROBLEM:地图实现为何复杂(3 类资源概述)

2. CREATE_TILE:瓦片 URL 拼接规则逆向 + 5 步逻辑拆解 + UrlBreakdown 分段可视化

3. ZOOM:缩放率与瓦片数量公式 t_max = (s/t_size) / 2^|z|,配合 ZoomLadder 交互组件 + TileGrid 真实瓦片预览

4. LEAFLET:底层库识别、L.CRS.Simple 平面坐标系、6 步渲染流水线、Mapsui for .NET 对比

5. DOWNLOAD:并行优化对比(1x → 6x),嵌套循环 vs 预加载 + Dispatcher

6. APIS:4 个 RESTful 端点 + tree→list 两段式查询 + isSea 彩蛋

7. PITFALLS:6 个真实踩坑(X/Y 颠倒、mapId 硬编码、Position 为空等)

8. COMMITS:7 次关键提交时间线

新增 5 个组件:TileGrid(瓦片网格)、UrlBreakdown(URL 拆解)、ZoomLadder(缩放阶梯)、ApiCard(API 卡片)、PitfallCard(踩坑卡片)

首页 chapters grid 改为 2x2,导航与 wikiCards 增加 /map 第 4 项
parent 2527dd12
Loading
Loading
Loading
Loading
+222 −0
Original line number Diff line number Diff line
<template>
  <div class="api-card" :data-tone="api.tone">
    <header class="api-card__head">
      <span class="api-card__tag">{{ api.key }}</span>
      <h3 class="api-card__title">{{ api.label }}</h3>
      <span class="api-card__method">{{ api.method }}</span>
    </header>

    <div class="api-card__url-wrap">
      <code class="api-card__url">{{ api.url }}</code>
      <button
        v-if="copied"
        class="api-card__copy is-copied"
        type="button"
        aria-label="已复制"
      >
        <CheckIcon :size="14" />
      </button>
      <button
        v-else
        class="api-card__copy"
        type="button"
        aria-label="复制 URL"
        @click="onCopy"
      >
        <CopyIcon :size="14" />
      </button>
    </div>

    <p class="api-card__desc">{{ api.desc }}</p>

    <div class="api-card__returns">
      <span class="api-card__returns-label">返回</span>
      <code class="api-card__returns-value">{{ api.returns }}</code>
    </div>
  </div>
</template>

<script setup lang="ts">
import { Check as CheckIcon, Copy as CopyIcon } from 'lucide-vue-next'
import type { ApiEndpoint } from '~/data/map'

interface Props {
  api: ApiEndpoint
}

const props = defineProps<Props>()

const copied = ref(false)
let copyTimer: ReturnType<typeof setTimeout> | null = null

const onCopy = async () => {
  try {
    await navigator.clipboard.writeText(props.api.url)
    copied.value = true
    if (copyTimer) clearTimeout(copyTimer)
    copyTimer = setTimeout(() => { copied.value = false }, 1500)
  } catch {
    // 剪贴板不可用时静默失败
  }
}

onBeforeUnmount(() => {
  if (copyTimer) clearTimeout(copyTimer)
})
</script>

<style scoped>
.api-card {
  background: var(--color-bg-elev);
  border: 1px solid var(--color-line);
  border-radius: var(--radius-lg);
  padding: var(--space-5);
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  position: relative;
  overflow: hidden;
  min-width: 0;
  transition: border-color var(--duration-base) var(--ease-out-quint),
              transform var(--duration-base) var(--ease-out-quint);
}

.api-card::before {
  content: '';
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  height: 2px;
  background: var(--tone-color, var(--color-accent));
  opacity: 0.6;
  transition: opacity var(--duration-base) var(--ease-out-quint);
}

.api-card:hover {
  border-color: var(--tone-color, var(--color-accent));
  transform: translateY(-2px);
}

.api-card:hover::before {
  opacity: 1;
}

.api-card[data-tone='anemo'] { --tone-color: #74C2A8; }
.api-card[data-tone='geo'] { --tone-color: #D4A45A; }
.api-card[data-tone='electro'] { --tone-color: #B68BE0; }
.api-card[data-tone='dendro'] { --tone-color: #9FCC4F; }
.api-card[data-tone='hydro'] { --tone-color: #4FC2D9; }
.api-card[data-tone='pyro'] { --tone-color: #E27B58; }
.api-card[data-tone='cryo'] { --tone-color: #9FE2BF; }

.api-card__head {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  flex-wrap: wrap;
}

.api-card__tag {
  font-family: var(--font-mono);
  font-size: 0.625rem;
  letter-spacing: 0.15em;
  color: var(--tone-color, var(--color-accent));
  padding: 2px 8px;
  background: rgba(212, 164, 90, 0.08);
  border-radius: 2px;
}

.api-card__title {
  font-family: var(--font-serif);
  font-size: 1rem;
  font-weight: 600;
  color: var(--color-text-primary);
  flex: 1;
}

.api-card__method {
  font-family: var(--font-mono);
  font-size: 0.5625rem;
  letter-spacing: 0.1em;
  color: var(--color-bg-base);
  background: var(--tone-color, var(--color-accent));
  padding: 1px 6px;
  border-radius: 2px;
  font-weight: 600;
}

.api-card__url-wrap {
  position: relative;
  background: var(--color-bg-deep);
  border: 1px solid var(--color-line);
  border-radius: var(--radius-md);
  padding: var(--space-3) var(--space-4);
  padding-right: 40px;
  overflow-x: auto;
}

.api-card__url {
  font-family: var(--font-mono);
  font-size: 0.6875rem;
  color: var(--color-text-secondary);
  word-break: break-all;
  line-height: 1.6;
  display: block;
}

.api-card__copy {
  position: absolute;
  top: 50%;
  right: 8px;
  transform: translateY(-50%);
  width: 24px;
  height: 24px;
  display: grid;
  place-items: center;
  background: transparent;
  border: 1px solid var(--color-line);
  border-radius: 4px;
  color: var(--color-text-muted);
  cursor: pointer;
  transition: all var(--duration-fast) var(--ease-out-quint);
}

.api-card__copy:hover {
  color: var(--color-accent);
  border-color: var(--color-accent);
}

.api-card__copy.is-copied {
  color: #74C2A8;
  border-color: #74C2A8;
}

.api-card__desc {
  font-size: 0.8125rem;
  color: var(--color-text-secondary);
  line-height: 1.6;
}

.api-card__returns {
  display: flex;
  align-items: baseline;
  gap: var(--space-3);
  padding-top: var(--space-3);
  border-top: 1px solid var(--color-line-faint);
}

.api-card__returns-label {
  font-family: var(--font-mono);
  font-size: 0.625rem;
  letter-spacing: 0.15em;
  color: var(--color-text-muted);
  text-transform: uppercase;
}

.api-card__returns-value {
  font-family: var(--font-mono);
  font-size: 0.75rem;
  color: var(--tone-color, var(--color-accent));
}
</style>
+145 −0
Original line number Diff line number Diff line
<template>
  <article class="pitfall-card" :data-tone="pitfall.tone">
    <div class="pitfall-card__bg" aria-hidden="true"></div>

    <header class="pitfall-card__head">
      <span class="pitfall-card__icon" aria-hidden="true">
        <AlertIcon :size="18" :stroke-width="1.6" />
      </span>
      <h3 class="pitfall-card__title">{{ pitfall.title }}</h3>
    </header>

    <div class="pitfall-card__body">
      <div class="pitfall-card__field">
        <span class="pitfall-card__field-label">症状</span>
        <p class="pitfall-card__field-value">{{ pitfall.symptom }}</p>
      </div>
      <div class="pitfall-card__field">
        <span class="pitfall-card__field-label">原因</span>
        <p class="pitfall-card__field-value">{{ pitfall.cause }}</p>
      </div>
      <div class="pitfall-card__field pitfall-card__field--fix">
        <span class="pitfall-card__field-label">修复</span>
        <p class="pitfall-card__field-value">{{ pitfall.fix }}</p>
      </div>
    </div>
  </article>
</template>

<script setup lang="ts">
import { AlertTriangle as AlertIcon } from 'lucide-vue-next'
import type { Pitfall } from '~/data/map'

defineProps<{ pitfall: Pitfall }>()
</script>

<style scoped>
.pitfall-card {
  position: relative;
  background: var(--color-bg-elev);
  border: 1px solid var(--color-line);
  border-radius: var(--radius-lg);
  padding: var(--space-5);
  overflow: hidden;
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
  min-width: 0;
  transition: border-color var(--duration-base) var(--ease-out-quint),
              transform var(--duration-base) var(--ease-out-quint);
}

.pitfall-card:hover {
  border-color: var(--tone-color, var(--color-accent));
  transform: translateY(-3px);
}

.pitfall-card[data-tone='anemo'] { --tone-color: #74C2A8; }
.pitfall-card[data-tone='geo'] { --tone-color: #D4A45A; }
.pitfall-card[data-tone='electro'] { --tone-color: #B68BE0; }
.pitfall-card[data-tone='dendro'] { --tone-color: #9FCC4F; }
.pitfall-card[data-tone='hydro'] { --tone-color: #4FC2D9; }
.pitfall-card[data-tone='pyro'] { --tone-color: #E27B58; }
.pitfall-card[data-tone='cryo'] { --tone-color: #9FE2BF; }

.pitfall-card__bg {
  position: absolute;
  inset: 0;
  background:
    radial-gradient(circle at 100% 0%, color-mix(in srgb, var(--tone-color) 12%, transparent), transparent 50%);
  opacity: 0.5;
  pointer-events: none;
  z-index: 0;
}

.pitfall-card__head {
  position: relative;
  z-index: 1;
  display: flex;
  align-items: center;
  gap: var(--space-3);
}

.pitfall-card__icon {
  display: grid;
  place-items: center;
  width: 32px;
  height: 32px;
  color: var(--tone-color, var(--color-accent));
  background: rgba(255, 255, 255, 0.04);
  border: 1px solid var(--color-line);
  border-radius: 50%;
  flex-shrink: 0;
}

.pitfall-card__title {
  font-family: var(--font-serif);
  font-size: 1rem;
  font-weight: 600;
  color: var(--color-text-primary);
  line-height: 1.3;
}

.pitfall-card__body {
  position: relative;
  z-index: 1;
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}

.pitfall-card__field {
  display: flex;
  flex-direction: column;
  gap: 4px;
}

.pitfall-card__field-label {
  font-family: var(--font-mono);
  font-size: 0.625rem;
  letter-spacing: 0.2em;
  color: var(--color-text-muted);
  text-transform: uppercase;
}

.pitfall-card__field-value {
  font-size: 0.8125rem;
  color: var(--color-text-secondary);
  line-height: 1.6;
}

.pitfall-card__field--fix {
  margin-top: 4px;
  padding-top: var(--space-3);
  border-top: 1px dashed var(--color-line);
}

.pitfall-card__field--fix .pitfall-card__field-label {
  color: var(--tone-color, var(--color-accent));
}

.pitfall-card__field--fix .pitfall-card__field-value {
  color: var(--color-text-primary);
  font-weight: 500;
}
</style>
+274 −0
Original line number Diff line number Diff line
<template>
  <div class="tile-grid" :data-tone="tone">
    <header class="tile-grid__head">
      <div class="tile-grid__head-left">
        <span class="tile-grid__tag">{{ tag }}</span>
        <h3 class="tile-grid__title">{{ title }}</h3>
      </div>
      <span class="tile-grid__meta">{{ cols }} × {{ rows }} = {{ cols * rows }} 瓦片</span>
    </header>

    <div class="tile-grid__scroll">
      <div
        class="tile-grid__board"
        :style="{ '--tile-size': tilePx + 'px' }"
      >
        <div
          v-for="(cell, i) in cells"
          :key="i"
          class="tile-grid__cell"
          :class="{
            'is-empty': !cell.url,
            'is-hovered': hoverKey === `${cell.x}_${cell.y}`
          }"
          @mouseenter="onHover(cell)"
          @mouseleave="onHover(null)"
        >
          <img
            v-if="cell.url"
            :src="cell.url"
            :alt="`tile ${cell.x}_${cell.y}`"
            loading="lazy"
            decoding="async"
            @error="onError(cell)"
          />
          <div v-if="!cell.url" class="tile-grid__cell-empty">
            <span>404</span>
          </div>
          <span class="tile-grid__cell-label">{{ cell.x }},{{ cell.y }}</span>
        </div>
      </div>
    </div>

    <footer v-if="hovered" class="tile-grid__foot">
      <code class="tile-grid__foot-url">{{ hovered.url || '(无瓦片)' }}</code>
    </footer>
  </div>
</template>

<script setup lang="ts">
interface TileCell {
  x: number
  y: number
  url: string | null
}

interface Props {
  title: string
  tag: string
  tone?: string
  mapId: number
  versionHash: string
  zoom: number
  cols?: number
  rows?: number
  tilePx?: number
}

const props = withDefaults(defineProps<Props>(), {
  tone: 'geo',
  cols: 11,
  rows: 8,
  tilePx: 48
})

const emit = defineEmits<{
  (e: 'hover', cell: TileCell | null): void
}>()

const zoomStr = computed(() => (props.zoom < 0 ? 'N' : 'P') + Math.abs(props.zoom))

const cells = computed<TileCell[]>(() => {
  const list: TileCell[] = []
  for (let y = 0; y < props.rows; y++) {
    for (let x = 0; x < props.cols; x++) {
      list.push({
        x,
        y,
        url: `https://act-webstatic.mihoyo.com/ys-map-op/map/${props.mapId}/${props.versionHash}/${x}_${y}_${zoomStr.value}.webp`
      })
    }
  }
  return list
})

const hovered = ref<TileCell | null>(null)
const hoverKey = computed(() => hovered.value ? `${hovered.value.x}_${hovered.value.y}` : null)
const failedSet = ref(new Set<string>())

const onHover = (cell: TileCell | null) => {
  hovered.value = cell
  emit('hover', cell)
}

const onError = (cell: TileCell) => {
  failedSet.value.add(`${cell.x}_${cell.y}`)
  cell.url = null
}
</script>

<style scoped>
.tile-grid {
  background: var(--color-bg-elev);
  border: 1px solid var(--color-line);
  border-radius: var(--radius-lg);
  overflow: hidden;
  display: flex;
  flex-direction: column;
  min-width: 0;
}

.tile-grid::before {
  content: '';
  display: block;
  height: 2px;
  background: var(--tone-color, var(--color-accent));
  opacity: 0.6;
}

.tile-grid[data-tone='geo'] { --tone-color: #74C2A8; }
.tile-grid[data-tone='hydro'] { --tone-color: #4FC2D9; }
.tile-grid[data-tone='cryo'] { --tone-color: #9FE2BF; }
.tile-grid[data-tone='electro'] { --tone-color: #B68BE0; }

.tile-grid__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-4);
  padding: var(--space-4) var(--space-5);
  border-bottom: 1px solid var(--color-line);
  background: rgba(212, 164, 90, 0.03);
}

.tile-grid__head-left {
  display: flex;
  align-items: baseline;
  gap: var(--space-3);
  min-width: 0;
}

.tile-grid__tag {
  font-family: var(--font-mono);
  font-size: 0.625rem;
  letter-spacing: 0.2em;
  color: var(--tone-color, var(--color-accent));
  white-space: nowrap;
}

.tile-grid__title {
  font-family: var(--font-serif);
  font-size: 1rem;
  font-weight: 600;
  color: var(--color-text-primary);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.tile-grid__meta {
  font-family: var(--font-mono);
  font-size: 0.6875rem;
  color: var(--color-text-muted);
  letter-spacing: 0.05em;
  white-space: nowrap;
}

.tile-grid__scroll {
  overflow: auto;
  padding: var(--space-4);
  background:
    linear-gradient(180deg, rgba(0,0,0,0.2), transparent 200px),
    var(--color-bg-deep);
}

.tile-grid__board {
  display: grid;
  grid-template-columns: repeat(v-bind(cols), var(--tile-size));
  grid-template-rows: repeat(v-bind(rows), var(--tile-size));
  gap: 2px;
  width: max-content;
  margin: 0 auto;
}

.tile-grid__cell {
  position: relative;
  width: var(--tile-size);
  height: var(--tile-size);
  background: rgba(255, 255, 255, 0.02);
  border: 1px solid var(--color-line-faint);
  overflow: hidden;
  transition: transform var(--duration-fast) var(--ease-out-quint),
              border-color var(--duration-fast) var(--ease-out-quint),
              box-shadow var(--duration-fast) var(--ease-out-quint);
  cursor: pointer;
}

.tile-grid__cell img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  image-rendering: pixelated;
}

.tile-grid__cell.is-empty {
  background: repeating-linear-gradient(
    45deg,
    rgba(212, 164, 90, 0.02),
    rgba(212, 164, 90, 0.02) 6px,
    transparent 6px,
    transparent 12px
  );
}

.tile-grid__cell-empty {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  font-family: var(--font-mono);
  font-size: 0.625rem;
  color: var(--color-text-muted);
  opacity: 0.5;
}

.tile-grid__cell-label {
  position: absolute;
  bottom: 1px;
  right: 2px;
  font-family: var(--font-mono);
  font-size: 0.5rem;
  color: var(--color-text-muted);
  opacity: 0;
  transition: opacity var(--duration-fast) var(--ease-out-quint);
  pointer-events: none;
}

.tile-grid__cell:hover,
.tile-grid__cell.is-hovered {
  transform: scale(1.18);
  z-index: 2;
  border-color: var(--tone-color, var(--color-accent));
  box-shadow: 0 0 16px rgba(212, 164, 90, 0.4);
}

.tile-grid__cell:hover .tile-grid__cell-label,
.tile-grid__cell.is-hovered .tile-grid__cell-label {
  opacity: 1;
}

.tile-grid__foot {
  padding: var(--space-3) var(--space-4);
  border-top: 1px solid var(--color-line);
  background: var(--color-bg-deep);
}

.tile-grid__foot-url {
  font-family: var(--font-mono);
  font-size: 0.6875rem;
  color: var(--color-text-secondary);
  word-break: break-all;
  display: block;
}
</style>
+227 −0
Original line number Diff line number Diff line
<template>
  <div class="url-break">
    <header class="url-break__head">
      <span class="url-break__tag">{{ tag }}</span>
      <h3 class="url-break__title">{{ title }}</h3>
    </header>

    <div class="url-break__url" :data-tone="tone">
      <span
        v-for="(seg, i) in segments"
        :key="i"
        class="url-break__seg"
        :class="{
          'is-active': activeKey === seg.key,
          'is-literal': seg.type === 'literal',
          'is-dynamic': seg.type === 'dynamic'
        }"
        @mouseenter="$emit('hover', seg.key)"
        @mouseleave="$emit('hover', null)"
      >
        <span class="url-break__seg-value">{{ seg.value }}</span>
        <span class="url-break__seg-label">{{ seg.label }}</span>
      </span>
    </div>

    <div class="url-break__legend">
      <div class="url-break__legend-item">
        <span class="url-break__legend-dot url-break__legend-dot--literal"></span>
        <span>固定字面量</span>
      </div>
      <div class="url-break__legend-item">
        <span class="url-break__legend-dot url-break__legend-dot--dynamic"></span>
        <span>动态参数</span>
      </div>
    </div>
  </div>
</template>

<script setup lang="ts">
interface UrlSegment {
  key: string
  label: string
  value: string
  type: 'literal' | 'dynamic'
}

interface Props {
  title: string
  tag: string
  tone?: string
  mapId: number
  versionHash: string
  x: number
  y: number
  z: number
  format?: 'webp' | 'png'
  activeKey?: string | null
}

const props = withDefaults(defineProps<Props>(), {
  tone: 'geo',
  format: 'webp',
  activeKey: null
})

defineEmits<{
  (e: 'hover', key: string | null): void
}>()

const zoomStr = computed(() => (props.z < 0 ? 'N' : 'P') + Math.abs(props.z))

const segments = computed<UrlSegment[]>(() => [
  { key: 'base', label: 'CDN 基址', value: 'https://act-webstatic.mihoyo.com/ys-map-op/map/', type: 'literal' },
  { key: 'mapId', label: '地图 ID', value: String(props.mapId), type: 'dynamic' },
  { key: 'slash1', label: '/', value: '/', type: 'literal' },
  { key: 'version', label: '版本 Hash', value: props.versionHash, type: 'dynamic' },
  { key: 'slash2', label: '/', value: '/', type: 'literal' },
  { key: 'x', label: '瓦片 X', value: String(props.x), type: 'dynamic' },
  { key: 'underscore1', label: '_', value: '_', type: 'literal' },
  { key: 'y', label: '瓦片 Y', value: String(props.y), type: 'dynamic' },
  { key: 'underscore2', label: '_', value: '_', type: 'literal' },
  { key: 'zoom', label: '缩放字符串', value: zoomStr.value, type: 'dynamic' },
  { key: 'dot', label: '.', value: '.', type: 'literal' },
  { key: 'format', label: '格式', value: props.format, type: 'dynamic' }
])
</script>

<style scoped>
.url-break {
  background: var(--color-bg-elev);
  border: 1px solid var(--color-line);
  border-radius: var(--radius-lg);
  padding: var(--space-6);
  min-width: 0;
  overflow: hidden;
}

.url-break__head {
  display: flex;
  align-items: baseline;
  gap: var(--space-3);
  margin-bottom: var(--space-5);
}

.url-break__tag {
  font-family: var(--font-mono);
  font-size: 0.625rem;
  letter-spacing: 0.2em;
  color: var(--color-accent);
  text-transform: uppercase;
}

.url-break__title {
  font-family: var(--font-serif);
  font-size: 1.125rem;
  font-weight: 600;
  color: var(--color-text-primary);
}

.url-break__url {
  display: flex;
  flex-wrap: wrap;
  align-items: stretch;
  gap: 2px;
  font-family: var(--font-mono);
  font-size: 0.8125rem;
  line-height: 1.5;
  padding: var(--space-4);
  background: var(--color-bg-deep);
  border: 1px solid var(--color-line);
  border-radius: var(--radius-md);
  overflow-x: auto;
}

.url-break__seg {
  display: inline-flex;
  flex-direction: column;
  align-items: stretch;
  padding: 4px 8px;
  border-radius: 3px;
  cursor: pointer;
  transition: background var(--duration-fast) var(--ease-out-quint),
              transform var(--duration-fast) var(--ease-out-quint);
  position: relative;
  min-width: 0;
}

.url-break__seg-value {
  display: block;
  white-space: nowrap;
  font-weight: 500;
}

.url-break__seg-label {
  display: block;
  font-size: 0.5625rem;
  letter-spacing: 0.1em;
  color: var(--color-text-muted);
  opacity: 0;
  height: 0;
  transition: opacity var(--duration-fast) var(--ease-out-quint),
              height var(--duration-fast) var(--ease-out-quint);
  text-transform: uppercase;
}

.url-break__seg.is-literal {
  color: var(--color-text-muted);
  background: rgba(255, 255, 255, 0.02);
}

.url-break__seg.is-dynamic {
  color: var(--color-accent);
  background: rgba(212, 164, 90, 0.08);
  border-bottom: 1px solid rgba(212, 164, 90, 0.3);
}

.url-break__seg.is-dynamic.is-active,
.url-break__seg.is-literal.is-active {
  background: rgba(212, 164, 90, 0.2);
  transform: translateY(-2px);
  box-shadow: 0 4px 12px rgba(212, 164, 90, 0.2);
}

.url-break__seg.is-active .url-break__seg-label,
.url-break__seg:hover .url-break__seg-label {
  opacity: 1;
  height: auto;
}

.url-break__legend {
  display: flex;
  gap: var(--space-5);
  margin-top: var(--space-4);
  font-family: var(--font-mono);
  font-size: 0.6875rem;
  color: var(--color-text-muted);
}

.url-break__legend-item {
  display: flex;
  align-items: center;
  gap: var(--space-2);
}

.url-break__legend-dot {
  display: inline-block;
  width: 10px;
  height: 10px;
  border-radius: 2px;
}

.url-break__legend-dot--literal {
  background: rgba(255, 255, 255, 0.1);
  border: 1px solid var(--color-line);
}

.url-break__legend-dot--dynamic {
  background: rgba(212, 164, 90, 0.3);
  border: 1px solid var(--color-accent);
}

@media (max-width: 768px) {
  .url-break__url {
    font-size: 0.6875rem;
  }
}
</style>
+245 −0

File added.

Preview size limit exceeded, changes collapsed.

Loading