Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9aebbbbde3 | ||
|
|
1dfbb0940d | ||
|
|
cd49408083 | ||
|
|
50621d6345 | ||
|
|
aade7d5b68 | ||
|
|
c853d3c344 | ||
|
|
dd50a10c62 | ||
|
|
69add8dcb7 | ||
|
|
202824b093 | ||
|
|
6cb91284f0 |
No files matched your search
@@ -68,6 +68,21 @@ test.describe('CourseCard 规格契约', () => {
|
|||||||
recipes.forEach((r) => expect(r.split('rgb(17, 17, 17)').length - 1).toBe(1)) // 每卡只出一份阴影
|
recipes.forEach((r) => expect(r.split('rgb(17, 17, 17)').length - 1).toBe(1)) // 每卡只出一份阴影
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('价值区标记用法卡:相框静置态过程文字带荧光 Mark', async ({ page }) => {
|
||||||
|
const m = await page.evaluate(() => {
|
||||||
|
const card = document.querySelector('a[href="#ch4"]')
|
||||||
|
const mark = card?.querySelector('mark')
|
||||||
|
if (!card || !mark) return null
|
||||||
|
const cs = getComputedStyle(mark)
|
||||||
|
return { bg: cs.backgroundColor, rot: cs.rotate, inZone: !!card.querySelector('.aspect-video mark'), txt: mark.textContent }
|
||||||
|
})
|
||||||
|
expect(m).not.toBeNull()
|
||||||
|
expect(m!.inZone).toBeTruthy() // 标记骑在相框内的过程文字上
|
||||||
|
expect(m!.bg).toBe('rgb(255, 217, 61)') // 荧光笔 mustard
|
||||||
|
expect(m!.rot).toContain('deg')
|
||||||
|
expect(m!.txt).toContain('咬合')
|
||||||
|
})
|
||||||
|
|
||||||
test('16:9 相框:比例、3px 黑边、内阴影、章节撞色', async ({ page }) => {
|
test('16:9 相框:比例、3px 黑边、内阴影、章节撞色', async ({ page }) => {
|
||||||
const zone = page.locator('a[href="#ch1"] .aspect-video').first()
|
const zone = page.locator('a[href="#ch1"] .aspect-video').first()
|
||||||
const s = await zone.evaluate((el) => {
|
const s = await zone.evaluate((el) => {
|
||||||
@@ -115,7 +130,7 @@ test.describe('RouteLine 规格契约', () => {
|
|||||||
expect(specs).toContain('4 2')
|
expect(specs).toContain('4 2')
|
||||||
})
|
})
|
||||||
|
|
||||||
test('站点 r6 半嵌:源骑卡右黑影外缘、目标贴左描边', async ({ page }) => {
|
test('站点 r6 半嵌:源/目标圆心都骑卡描边(各半在卡上),线层在卡之上', async ({ page }) => {
|
||||||
const c1 = await page.locator('a[href="#ch1"]').first().boundingBox()
|
const c1 = await page.locator('a[href="#ch1"]').first().boundingBox()
|
||||||
const c2 = await page.locator('a[href="#ch2"]').first().boundingBox()
|
const c2 = await page.locator('a[href="#ch2"]').first().boundingBox()
|
||||||
const wrap = await page.locator('main div.relative').first().boundingBox()
|
const wrap = await page.locator('main div.relative').first().boundingBox()
|
||||||
@@ -126,10 +141,17 @@ test.describe('RouteLine 规格契约', () => {
|
|||||||
)
|
)
|
||||||
expect(stations.length).toBeGreaterThanOrEqual(4)
|
expect(stations.length).toBeGreaterThanOrEqual(4)
|
||||||
|
|
||||||
const c1ShadowEdge = c1!.x - wrap!.x + c1!.width + 7 // 描边外 + 7px 黑影
|
const c1Right = c1!.x - wrap!.x + c1!.width // 卡右描边(不接阴影)
|
||||||
const c2Border = c2!.x - wrap!.x
|
const c2Border = c2!.x - wrap!.x
|
||||||
expect(stations.some((s) => Math.abs(s.cx - c1ShadowEdge) <= 3), `源站点应骑黑影外缘 x≈${c1ShadowEdge}`).toBeTruthy()
|
expect(stations.some((s) => Math.abs(s.cx - c1Right) <= 3), `源站点圆心应骑卡右描边 x≈${c1Right}`).toBeTruthy()
|
||||||
expect(stations.some((s) => Math.abs(s.cx - c2Border) <= 3), `目标站点应贴左描边 x≈${c2Border}`).toBeTruthy()
|
expect(stations.some((s) => Math.abs(s.cx - c2Border) <= 3), `目标站点圆心应骑左描边 x≈${c2Border}`).toBeTruthy()
|
||||||
|
|
||||||
|
// 线层必须置于卡之上——否则半嵌的站点被卡盖住,只剩卡外一小块
|
||||||
|
const z = await page.evaluate(() => {
|
||||||
|
const svg = document.querySelector('main div.relative > svg') as SVGElement | null
|
||||||
|
return svg ? getComputedStyle(svg).zIndex : 'missing'
|
||||||
|
})
|
||||||
|
expect(z).toBe('10')
|
||||||
})
|
})
|
||||||
|
|
||||||
test('实线端点落在水平端口(M x y H 起、H x 止)', async ({ page }) => {
|
test('实线端点落在水平端口(M x y H 起、H x 止)', async ({ page }) => {
|
||||||
@@ -139,6 +161,63 @@ test.describe('RouteLine 规格契约', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test.describe('散落形状摆放契约', () => {
|
||||||
|
test.beforeEach(async ({ page }) => await page.goto('/shelf.html'))
|
||||||
|
|
||||||
|
test('卡片附近空当+随机:不压卡、都在卡附近、不出容器、旋转各异', async ({ page }) => {
|
||||||
|
const r = await page.evaluate(() => {
|
||||||
|
const layer = document.querySelector('[data-scatter-layer]') as HTMLElement | null
|
||||||
|
if (!layer) return null
|
||||||
|
const host = layer.parentElement as HTMLElement
|
||||||
|
const hb = host.getBoundingClientRect()
|
||||||
|
const shapes = [...layer.querySelectorAll('[data-scatter]')] as HTMLElement[]
|
||||||
|
const comps = [...host.querySelectorAll('[data-comp]')].map((el) => el.getBoundingClientRect())
|
||||||
|
const hit = (a: DOMRect, b: DOMRect) => a.left < b.right && a.right > b.left && a.top < b.bottom && a.bottom > b.top
|
||||||
|
return {
|
||||||
|
n: shapes.length,
|
||||||
|
inHost: shapes.every((s) => {
|
||||||
|
const r = s.getBoundingClientRect()
|
||||||
|
return r.left >= hb.left - 1 && r.right <= hb.right + 1 && r.top >= hb.top - 1 && r.bottom <= hb.bottom + 1
|
||||||
|
}),
|
||||||
|
onComp: shapes.filter((s) => comps.some((c) => hit(s.getBoundingClientRect(), c))).length,
|
||||||
|
nearAll: shapes.every((s) => {
|
||||||
|
const r = s.getBoundingClientRect()
|
||||||
|
const cx = (r.left + r.right) / 2
|
||||||
|
const cy = (r.top + r.bottom) / 2
|
||||||
|
return comps.some((c) => cx >= c.left - 200 && cx <= c.right + 200 && cy >= c.top - 200 && cy <= c.bottom + 200)
|
||||||
|
}),
|
||||||
|
transforms: shapes.map((s) => s.style.transform),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
expect(r).not.toBeNull()
|
||||||
|
expect(r!.n).toBeGreaterThanOrEqual(5)
|
||||||
|
expect(r!.inHost).toBeTruthy() // 不出场景容器
|
||||||
|
expect(r!.onComp).toBe(0) // 空当=不压任何内容盒
|
||||||
|
expect(r!.nearAll).toBeTruthy() // 只散在卡片附近,不胡乱撒
|
||||||
|
expect(new Set(r!.transforms).size).toBe(r!.n) // 落点/旋转各不相同
|
||||||
|
r!.transforms.forEach((t) => expect(t).toMatch(/^translate\(-50%, -50%\) rotate\(-?\d+deg\)$/))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test.describe('价值区荧光标记契约', () => {
|
||||||
|
test.beforeEach(async ({ page }) => await page.goto('/symbols.html'))
|
||||||
|
|
||||||
|
test('荧光笔标记文字:mustard 底、无描边、微斜——是标记不是形状', async ({ page }) => {
|
||||||
|
const s = await page.evaluate(() => {
|
||||||
|
const m = document.querySelector('mark')
|
||||||
|
if (!m) return null
|
||||||
|
const cs = getComputedStyle(m)
|
||||||
|
const r = m.getBoundingClientRect()
|
||||||
|
return { bg: cs.backgroundColor, bw: cs.borderTopWidth, rot: cs.rotate, h: r.height }
|
||||||
|
})
|
||||||
|
expect(s).not.toBeNull()
|
||||||
|
expect(s!.bg).toBe('rgb(255, 217, 61)') // clash2a mustard——荧光笔
|
||||||
|
expect(s!.bw).toBe('0px') // 标记无描边:2px ink 是形状的规则,标记划在文字上
|
||||||
|
expect(s!.rot).toContain('deg') // 微斜(rotate 属性生效,手划手感)
|
||||||
|
expect(s!.h).toBeGreaterThan(14) // 骑在文字上,有字高
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
test.describe('票券规格契约', () => {
|
test.describe('票券规格契约', () => {
|
||||||
test.beforeEach(async ({ page }) => await page.goto('/symbols.html'))
|
test.beforeEach(async ({ page }) => await page.goto('/symbols.html'))
|
||||||
|
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 104 KiB After Width: | Height: | Size: 182 KiB |
|
Before Width: | Height: | Size: 176 KiB After Width: | Height: | Size: 155 KiB |
|
Before Width: | Height: | Size: 115 KiB After Width: | Height: | Size: 135 KiB |
|
Before Width: | Height: | Size: 179 KiB After Width: | Height: | Size: 187 KiB |
@@ -2,7 +2,7 @@
|
|||||||
* 卡头三件套=靶子:▲目标 + ○预期结果(16:9 相框) + ●积木点列
|
* 卡头三件套=靶子:▲目标 + ○预期结果(16:9 相框) + ●积木点列
|
||||||
* 交互:hover/聚焦拿起(浮起+投影加深)并播放 GIF;desc ⇄ GIF 每 8000ms 翻转;
|
* 交互:hover/聚焦拿起(浮起+投影加深)并播放 GIF;desc ⇄ GIF 每 8000ms 翻转;
|
||||||
* hover/Tab 锁定;触屏与 prefers-reduced-motion 不翻转。 */
|
* hover/Tab 锁定;触屏与 prefers-reduced-motion 不翻转。 */
|
||||||
import { useEffect, useRef, useState } from 'react'
|
import { useEffect, useRef, useState, type ReactNode } from 'react'
|
||||||
import type { Chapter } from '../lib/types'
|
import type { Chapter } from '../lib/types'
|
||||||
import { Ring, Tri } from './shapes'
|
import { Ring, Tri } from './shapes'
|
||||||
|
|
||||||
@@ -33,7 +33,18 @@ export function EntryBadge({ n }: { n: 1 | 2 | 3 }) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function MemphisCard({ chapter, entry, compact = false }: { chapter: Chapter; entry?: 1 | 2 | 3; compact?: boolean }) {
|
export function MemphisCard({
|
||||||
|
chapter,
|
||||||
|
entry,
|
||||||
|
compact = false,
|
||||||
|
zoneText,
|
||||||
|
}: {
|
||||||
|
chapter: Chapter
|
||||||
|
entry?: 1 | 2 | 3
|
||||||
|
compact?: boolean
|
||||||
|
/** 静置态相框文字(可含 <Mark> 荧光标记):教程卡片的价值区用法示例 */
|
||||||
|
zoneText?: ReactNode
|
||||||
|
}) {
|
||||||
const [gifOn, setGifOn] = useState(false)
|
const [gifOn, setGifOn] = useState(false)
|
||||||
const [locked, setLocked] = useState(false)
|
const [locked, setLocked] = useState(false)
|
||||||
const boxRef = useRef<HTMLAnchorElement>(null)
|
const boxRef = useRef<HTMLAnchorElement>(null)
|
||||||
@@ -100,6 +111,9 @@ export function MemphisCard({ chapter, entry, compact = false }: { chapter: Chap
|
|||||||
<span className="px-3 text-center text-[11px] leading-snug text-ink/80 mix-blend-multiply">
|
<span className="px-3 text-center text-[11px] leading-snug text-ink/80 mix-blend-multiply">
|
||||||
{chapter.target.goal}
|
{chapter.target.goal}
|
||||||
</span>
|
</span>
|
||||||
|
) : zoneText ? (
|
||||||
|
/* 静置态(用法示例):过程文字骑在相框里,价值区短语带荧光标记 */
|
||||||
|
<span className="px-3 text-center text-[11.5px] font-semibold leading-snug text-ink">{zoneText}</span>
|
||||||
) : (
|
) : (
|
||||||
/* 静置态:章节 emoji 是相框的视觉主体(对齐设计稿解剖卡) */
|
/* 静置态:章节 emoji 是相框的视觉主体(对齐设计稿解剖卡) */
|
||||||
<span style={{ fontSize: compact ? '28px' : '40px' }}>{chapter.emoji}</span>
|
<span style={{ fontSize: compact ? '28px' : '40px' }}>{chapter.emoji}</span>
|
||||||
|
|||||||
@@ -1,14 +1,15 @@
|
|||||||
/** RouteLine —— 关系线:线形即含义(实=先后 虚=相关 点=同族)
|
/** RouteLine —— 关系线:线形即含义(实=先后 虚=相关 点=同族)
|
||||||
* 拓扑:横平竖直 + R8 直角步进;并行不合并(4px);站点 r6 半嵌卡边
|
* 拓扑:横平竖直 + R8 直角步进;并行不合并(4px);站点 r6 半嵌卡边
|
||||||
* (左贴描边、右骑硬投影外缘);同口多线自动聚合胶囊(线止于胶囊边界)。 */
|
* (圆心骑左右描边,各半在卡上);线层 z-10 置于卡之上,站点不被卡遮挡;
|
||||||
|
* 同口多线自动聚合胶囊(线止于胶囊边界)。 */
|
||||||
import type { ChapterId, Edge } from '../lib/types'
|
import type { ChapterId, Edge } from '../lib/types'
|
||||||
|
|
||||||
const STATION_COLORS = ['var(--color-clash2a)', 'var(--color-clash1b)', 'var(--color-clash3a)', 'var(--color-clash1a)']
|
const STATION_COLORS = ['var(--color-clash2a)', 'var(--color-clash1b)', 'var(--color-clash3a)', 'var(--color-clash1a)']
|
||||||
|
|
||||||
export interface LaneNode {
|
export interface LaneNode {
|
||||||
id: ChapterId
|
id: ChapterId
|
||||||
borderLeft: number // 左描边 x(无影侧,入口贴这里)
|
borderLeft: number // 左描边 x(入口圆心贴这里,点半嵌)
|
||||||
right: number // 右侧黑影外缘 x(描边 + 7,出口从这里出发)
|
right: number // 右描边 x(出口圆心贴这里,点半嵌——不接阴影)
|
||||||
y: number // 卡中线 y
|
y: number // 卡中线 y
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,7 +48,7 @@ export function RouteLine({
|
|||||||
width={width}
|
width={width}
|
||||||
height={height}
|
height={height}
|
||||||
viewBox={`0 0 ${width} ${height}`}
|
viewBox={`0 0 ${width} ${height}`}
|
||||||
className="pointer-events-none absolute left-0 top-0"
|
className="pointer-events-none absolute left-0 top-0 z-10"
|
||||||
aria-hidden
|
aria-hidden
|
||||||
>
|
>
|
||||||
{edges.map((e, i) => {
|
{edges.map((e, i) => {
|
||||||
@@ -65,7 +66,7 @@ export function RouteLine({
|
|||||||
if (!a || !b) return null
|
if (!a || !b) return null
|
||||||
const ins = insByTo.get(e.to)!
|
const ins = insByTo.get(e.to)!
|
||||||
const capsule = ins.length > 1 && firstIn.has(e.to)
|
const capsule = ins.length > 1 && firstIn.has(e.to)
|
||||||
// 源站点骑出边卡黑影外缘;目标:单线=圆点贴描边,多线=胶囊(只画一次)
|
// 源/目标站点:圆心骑卡描边,各半在卡上;目标:单线=圆点,多线=胶囊(只画一次)
|
||||||
return (
|
return (
|
||||||
<g key={`st-${e.from}-${e.to}`}>
|
<g key={`st-${e.from}-${e.to}`}>
|
||||||
<circle cx={a.right} cy={a.y} r={6} fill={stationColor(si++)} stroke="var(--color-ink)" strokeWidth={2} />
|
<circle cx={a.right} cy={a.y} r={6} fill={stationColor(si++)} stroke="var(--color-ink)" strokeWidth={2} />
|
||||||
|
|||||||
@@ -0,0 +1,90 @@
|
|||||||
|
/** 散落形状摆放——「卡片附近空当 + 随机」:
|
||||||
|
* 网格采样容器空白 → 过滤出距内容盒([data-comp])≤ near 的空当(不胡乱撒)→
|
||||||
|
* 种子随机挑位(彼此保距)+空当内随机偏移+随机旋转。
|
||||||
|
* 种子固定 → 每次渲染一致(视觉基线可测);容器重排(ResizeObserver)自动重算。 */
|
||||||
|
import { useLayoutEffect, useRef, useState, type ReactNode } from 'react'
|
||||||
|
|
||||||
|
function mulberry32(seed: number) {
|
||||||
|
let t = seed >>> 0
|
||||||
|
return () => {
|
||||||
|
t = (t + 0x6d2b79f5) >>> 0
|
||||||
|
let x = Math.imul(t ^ (t >>> 15), 1 | t)
|
||||||
|
x = (x + Math.imul(x ^ (x >>> 7), 61 | x)) ^ x
|
||||||
|
return ((x ^ (x >>> 14)) >>> 0) / 4294967296
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Place = { x: number; y: number; rot: number }
|
||||||
|
|
||||||
|
export function Scatter({
|
||||||
|
shapes,
|
||||||
|
seed = 7,
|
||||||
|
near = 150, // 空当须距某内容盒 ≤ near——只散在卡片附近
|
||||||
|
margin = 34, // 与内容盒保持的最小间隙(≥ 最大形状半对角)
|
||||||
|
step = 30, // 采样网格密度
|
||||||
|
minGap = 80, // 形状彼此最小间距
|
||||||
|
}: {
|
||||||
|
shapes: ReactNode[]
|
||||||
|
seed?: number
|
||||||
|
near?: number
|
||||||
|
margin?: number
|
||||||
|
step?: number
|
||||||
|
minGap?: number
|
||||||
|
}) {
|
||||||
|
const layerRef = useRef<HTMLDivElement>(null)
|
||||||
|
const [places, setPlaces] = useState<Place[]>([])
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
const layer = layerRef.current
|
||||||
|
const host = layer?.parentElement
|
||||||
|
if (!layer || !host) return
|
||||||
|
|
||||||
|
const place = () => {
|
||||||
|
const hb = host.getBoundingClientRect()
|
||||||
|
const rects = [...host.querySelectorAll('[data-comp]')].map((el) => el.getBoundingClientRect())
|
||||||
|
if (!rects.length || !hb.width) return
|
||||||
|
const dist = (x: number, y: number, r: DOMRect) =>
|
||||||
|
Math.hypot(Math.max(r.left - x, 0, x - r.right), Math.max(r.top - y, 0, y - r.bottom))
|
||||||
|
const cand: { x: number; y: number }[] = []
|
||||||
|
for (let x = 36; x < hb.width - 36; x += step)
|
||||||
|
for (let y = 36; y < hb.height - 36; y += step) {
|
||||||
|
const px = hb.left + x
|
||||||
|
const py = hb.top + y
|
||||||
|
if (rects.every((r) => dist(px, py, r) >= margin) && rects.some((r) => dist(px, py, r) <= near))
|
||||||
|
cand.push({ x, y })
|
||||||
|
}
|
||||||
|
const rnd = mulberry32(seed)
|
||||||
|
for (let i = cand.length - 1; i > 0; i--) {
|
||||||
|
const j = Math.floor(rnd() * (i + 1))
|
||||||
|
;[cand[i], cand[j]] = [cand[j], cand[i]]
|
||||||
|
}
|
||||||
|
const taken: Place[] = []
|
||||||
|
for (const c of cand) {
|
||||||
|
if (taken.length >= shapes.length) break
|
||||||
|
if (taken.some((t) => Math.hypot(t.x - c.x, t.y - c.y) < minGap)) continue
|
||||||
|
taken.push({ x: c.x + (rnd() - 0.5) * 14, y: c.y + (rnd() - 0.5) * 14, rot: Math.round((rnd() - 0.5) * 52) })
|
||||||
|
}
|
||||||
|
setPlaces(taken)
|
||||||
|
}
|
||||||
|
|
||||||
|
place()
|
||||||
|
const ro = new ResizeObserver(place)
|
||||||
|
ro.observe(host)
|
||||||
|
return () => ro.disconnect()
|
||||||
|
}, [seed, near, margin, step, minGap, shapes.length])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={layerRef} data-scatter-layer aria-hidden className="pointer-events-none absolute inset-0">
|
||||||
|
{places.map((p, i) => (
|
||||||
|
<div
|
||||||
|
key={i}
|
||||||
|
data-scatter
|
||||||
|
className="absolute"
|
||||||
|
style={{ left: p.x, top: p.y, transform: `translate(-50%, -50%) rotate(${p.rot}deg)` }}
|
||||||
|
>
|
||||||
|
{shapes[i]}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
/** 七个语义形状——装饰即语义(取自 ai-era-tutorial 词汇表) */
|
/** 七个语义形状——装饰即语义(取自 ai-era-tutorial 词汇表) */
|
||||||
import type { CSSProperties } from 'react'
|
import type { CSSProperties, ReactNode } from 'react'
|
||||||
|
|
||||||
interface ShapeProps {
|
interface ShapeProps {
|
||||||
size?: number
|
size?: number
|
||||||
@@ -36,11 +36,14 @@ export const Wave = ({ size = 40, className, style }: ShapeProps) => (
|
|||||||
</svg>
|
</svg>
|
||||||
)
|
)
|
||||||
|
|
||||||
/** ▬ 价值区:实心条,过程就地写清 */
|
/** 荧光标记 价值区:色块骑在文字背后——是标记不是形状,
|
||||||
export const Bar = ({ size = 34, className, style }: ShapeProps) => (
|
* 无描边、微斜、两端比字宽(荧光笔划过的手感),过程就地写清 */
|
||||||
<svg width={size} height={size * 0.58} viewBox="0 0 34 20" className={className} style={style} aria-hidden>
|
export const Mark = ({ children, className }: { children: ReactNode; className?: string }) => (
|
||||||
<rect x="2" y="6" width="30" height="9" fill="var(--color-clash3a)" stroke="var(--color-ink)" strokeWidth="2" />
|
<mark
|
||||||
</svg>
|
className={`-rotate-1 rounded-[3px] bg-clash2a px-[4px] py-[1px] font-bold text-ink [box-decoration-break:clone] ${className ?? ''}`}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</mark>
|
||||||
)
|
)
|
||||||
|
|
||||||
/** ⌇ 脱靶:竖立颤抖——图形入口,读者追随图形进入阅读 */
|
/** ⌇ 脱靶:竖立颤抖——图形入口,读者追随图形进入阅读 */
|
||||||
|
|||||||
@@ -24,6 +24,34 @@ function Chip({ c, n, h }: { c: string; n: string; h: string }) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 三条规则的「为什么」:鲜艳如何成立、如何被收束、注意力如何分配 */
|
||||||
|
const RULES = [
|
||||||
|
{
|
||||||
|
name: '相邻必撞色(跨对取色)',
|
||||||
|
why: '孟菲斯的鲜艳靠碰撞成立:两个高饱和色块相邻时,只有色相拉开距离,视觉才读得出「两个声音」。同色相邻只是一个声音变大——画面发平发闷,鲜艳互相抵消;跨对取色(coral 挨 teal、mustard 挨 violet)反而把彼此衬得更亮。拿到相邻色块时问一句「它俩是不是同一对」,是,就换掉一个。',
|
||||||
|
badCap: '同色相邻失去碰撞',
|
||||||
|
goodCap: '相邻必撞色(跨对取色)',
|
||||||
|
badDemo: <div className="flex overflow-hidden rounded-lg border-2 border-ink"><div className="h-10 w-20 bg-clash1a" /><div className="h-10 w-20 bg-clash1a" /></div>,
|
||||||
|
goodDemo: <div className="flex overflow-hidden rounded-lg border-2 border-ink"><div className="h-10 w-20 bg-clash1a" /><div className="h-10 w-20 bg-clash1b" /></div>,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '黑描边收束鲜艳',
|
||||||
|
why: '高饱和色放在浅纸上会「振动」:边缘发虚、像悬浮在页面上。2px ink 描边给每块鲜艳一道明确的边界,把颜色关进形状里——这也是拟物卡世界的基础:卡片、形状、站点共用同一粗细的墨线,颜色再艳也是「零件」而不是「光」,缩放与低分屏上轮廓依然清晰。去掉描边,鲜艳就是没裁过的布料。',
|
||||||
|
badCap: '无描边的鲜艳漂浮刺眼',
|
||||||
|
goodCap: '一切鲜艳以 2px ink 描边为界',
|
||||||
|
badDemo: <div className="h-10 w-40 rounded-lg bg-clash2a" />,
|
||||||
|
goodDemo: <div className="h-10 w-40 rounded-lg border-2 border-ink bg-clash2a shadow-hard" />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '大底纸色,鲜艳只进局部',
|
||||||
|
why: '鲜艳是注意力,铺满屏幕等于没有注意力。大底永远是 paper,鲜艳只进相框、站点、散落形状这些局部(≤10%/屏),视线才有落点、正文才永远可读;大底一旦鲜艳,字被淹没,卡片也失去「纸上的物」的载体感。记住层级:大底是世界,卡片是物,鲜艳是物上的标记。',
|
||||||
|
badCap: '大底鲜艳,字被淹没',
|
||||||
|
goodCap: '大底纸色,鲜艳只进局部',
|
||||||
|
badDemo: <div className="flex h-10 w-40 items-center justify-center rounded-lg border-2 border-ink bg-clash1b text-[11px] font-bold">大底鲜艳</div>,
|
||||||
|
goodDemo: <div className="flex h-10 w-40 items-center justify-center rounded-lg border-2 border-ink bg-paper text-[11px] font-bold">纸底安放正文</div>,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
function DoDont({ bad, children, text }: { bad?: boolean; children: React.ReactNode; text: string }) {
|
function DoDont({ bad, children, text }: { bad?: boolean; children: React.ReactNode; text: string }) {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-1 items-center gap-3">
|
<div className="flex flex-1 items-center gap-3">
|
||||||
@@ -59,26 +87,21 @@ export function App() {
|
|||||||
<Section label="章节色" note="六章六色,只进 16:9 相框与图标瓦">
|
<Section label="章节色" note="六章六色,只进 16:9 相框与图标瓦">
|
||||||
<div className="grid grid-cols-6 gap-3">{CHAPTERS6.map((x) => <Chip key={x.n} {...x} h={x.c} />)}</div>
|
<div className="grid grid-cols-6 gap-3">{CHAPTERS6.map((x) => <Chip key={x.n} {...x} h={x.c} />)}</div>
|
||||||
</Section>
|
</Section>
|
||||||
<Section label="规则与反例" note="✗ 禁止 vs ✓ 正确">
|
<Section label="规则与反例" note="先讲为什么这样设计,再看 ✗/✓ 对照">
|
||||||
<div className="space-y-4">
|
<div className="space-y-5">
|
||||||
<DoDont bad text="同色相邻失去碰撞">
|
{RULES.map((r, i) => (
|
||||||
<div className="flex overflow-hidden rounded-lg border-2 border-ink"><div className="h-10 w-20 bg-clash1a" /><div className="h-10 w-20 bg-clash1a" /></div>
|
<div key={r.name}>
|
||||||
</DoDont>
|
<div className="flex items-baseline gap-2">
|
||||||
<DoDont text="相邻必撞色(跨对取色)">
|
<span className="flex h-[18px] w-[18px] shrink-0 translate-y-[2px] items-center justify-center rounded-full border-2 border-ink bg-ink text-[10px] font-extrabold text-paper">{i + 1}</span>
|
||||||
<div className="flex overflow-hidden rounded-lg border-2 border-ink"><div className="h-10 w-20 bg-clash1a" /><div className="h-10 w-20 bg-clash1b" /></div>
|
<b className="text-[13px]">{r.name}</b>
|
||||||
</DoDont>
|
</div>
|
||||||
<DoDont bad text="无描边的鲜艳漂浮刺眼">
|
<p className="m-0 mt-1.5 mb-2 max-w-[760px] text-[11.5px] leading-relaxed text-body">{r.why}</p>
|
||||||
<div className="h-10 w-40 rounded-lg bg-clash2a" />
|
<div className="grid grid-cols-2 gap-3">
|
||||||
</DoDont>
|
<DoDont bad text={r.badCap}>{r.badDemo}</DoDont>
|
||||||
<DoDont text="一切鲜艳以 2px ink 描边为界">
|
<DoDont text={r.goodCap}>{r.goodDemo}</DoDont>
|
||||||
<div className="h-10 w-40 rounded-lg border-2 border-ink bg-clash2a shadow-hard" />
|
</div>
|
||||||
</DoDont>
|
</div>
|
||||||
<DoDont bad text="大底鲜艳,字被淹没">
|
))}
|
||||||
<div className="flex h-10 w-40 items-center justify-center rounded-lg border-2 border-ink bg-clash1b text-[11px] font-bold">大底鲜艳</div>
|
|
||||||
</DoDont>
|
|
||||||
<DoDont text="大底纸色,鲜艳只进局部">
|
|
||||||
<div className="flex h-10 w-40 items-center justify-center rounded-lg border-2 border-ink bg-paper text-[11px] font-bold">纸底安放正文</div>
|
|
||||||
</DoDont>
|
|
||||||
</div>
|
</div>
|
||||||
</Section>
|
</Section>
|
||||||
</Page>
|
</Page>
|
||||||
|
|||||||
@@ -1,32 +1,28 @@
|
|||||||
import { Page, Section } from '../../lib/ui'
|
import { Page, Section } from '../../lib/ui'
|
||||||
import { ASSIGNMENT, CHAPTERS } from '../../lib/chapters'
|
import { ASSIGNMENT, CHAPTERS } from '../../lib/chapters'
|
||||||
import { MemphisCard } from '../../components/MemphisCard'
|
import { MemphisCard } from '../../components/MemphisCard'
|
||||||
import { RouteLine } from '../../components/RouteLine'
|
|
||||||
import { AssignBlock, Ticket } from '../../components/Ticket'
|
import { AssignBlock, Ticket } from '../../components/Ticket'
|
||||||
import { EntryGuide } from '../../components/EntryGuide'
|
import { EntryGuide } from '../../components/EntryGuide'
|
||||||
|
import { Mark } from '../../components/shapes'
|
||||||
const nodes = {
|
|
||||||
1: { id: 1, borderLeft: 40, right: 299, y: 180 },
|
|
||||||
2: { id: 2, borderLeft: 378, right: 639, y: 108 },
|
|
||||||
5: { id: 5, borderLeft: 699, right: 848, y: 108 },
|
|
||||||
} as const
|
|
||||||
|
|
||||||
export function App() {
|
export function App() {
|
||||||
return (
|
return (
|
||||||
<Page tag="<Components />" title="组件 · 入口载体与关系" desc="CourseCard(入口)· RouteLine(关系)· 票券(状态)· 指派块+脱靶标记(任务与警示)· 入口画像。">
|
<Page
|
||||||
<Section label="CourseCard" note="卡头三件套=靶子;hover 拿起播放 GIF,8000ms 翻转">
|
tag="<Components />"
|
||||||
|
title="组件 · 入口载体与状态"
|
||||||
|
desc="CourseCard(入口)· 票券(状态)· 指派块+脱靶标记(任务与警示)· 入口画像。线条的形式规格见 02 符号形状,与卡片的实际组合见 04 样例。"
|
||||||
|
>
|
||||||
|
<Section label="CourseCard" note="卡头三件套=靶子;hover 拿起播放 GIF,8000ms 翻转;末张=价值区标记用法">
|
||||||
<div className="flex flex-wrap items-start gap-8">
|
<div className="flex flex-wrap items-start gap-8">
|
||||||
<div className="w-[280px]"><MemphisCard chapter={CHAPTERS[1]} entry={1} /></div>
|
<div className="w-[280px]"><MemphisCard chapter={CHAPTERS[1]} entry={1} /></div>
|
||||||
<div className="w-[280px]"><MemphisCard chapter={CHAPTERS[2]} entry={2} /></div>
|
<div className="w-[280px]"><MemphisCard chapter={CHAPTERS[2]} entry={2} /></div>
|
||||||
<div className="w-[160px]"><MemphisCard chapter={CHAPTERS[5]} compact /></div>
|
<div className="w-[160px]"><MemphisCard chapter={CHAPTERS[5]} compact /></div>
|
||||||
</div>
|
<div className="w-[280px]">
|
||||||
</Section>
|
<MemphisCard
|
||||||
<Section label="RouteLine" note="实=先后 虚=相关 点=同族;站点半嵌(左贴描边、右骑黑影外缘)">
|
chapter={CHAPTERS[4]}
|
||||||
<div className="relative h-[240px] rounded-2xl border-2 border-dashed border-[#cfcfcd] bg-[#fbfbfa] p-0">
|
zoneText={<>这一步过程就地写清:<Mark>环境与依赖如何咬合</Mark></>}
|
||||||
<RouteLine edges={[{ from: 1, to: 2, kind: 'seq' }, { from: 2, to: 5, kind: 'seq' }, { from: 2, to: 5, kind: 'family' }]} nodes={nodes} width={880} height={240} />
|
/>
|
||||||
<div className="absolute left-[40px] top-[72px] w-[250px]"><MemphisCard chapter={CHAPTERS[1]} /></div>
|
</div>
|
||||||
<div className="absolute left-[380px] top-0 w-[250px]"><MemphisCard chapter={CHAPTERS[2]} /></div>
|
|
||||||
<div className="absolute left-[700px] top-0 w-[150px]"><MemphisCard chapter={CHAPTERS[5]} compact /></div>
|
|
||||||
</div>
|
</div>
|
||||||
</Section>
|
</Section>
|
||||||
<Section label="票券 & 指派学习块" note="票=完成状态;块=位置/预期/场景+脱靶标记">
|
<Section label="票券 & 指派学习块" note="票=完成状态;块=位置/预期/场景+脱靶标记">
|
||||||
|
|||||||
@@ -1,48 +1,49 @@
|
|||||||
|
/** 组合场景=大部分组件拿出来的真实使用,不做解释性标注:
|
||||||
|
* 入口画像 → 课程卡与关系线 → 进度票券 → 指派块;形状散落于卡片附近空当。 */
|
||||||
import { Page } from '../../lib/ui'
|
import { Page } from '../../lib/ui'
|
||||||
import { ASSIGNMENT, CHAPTERS, EDGES } from '../../lib/chapters'
|
import { ASSIGNMENT, CHAPTERS, EDGES } from '../../lib/chapters'
|
||||||
import { MemphisCard } from '../../components/MemphisCard'
|
import { MemphisCard } from '../../components/MemphisCard'
|
||||||
import { RouteLine } from '../../components/RouteLine'
|
import { RouteLine } from '../../components/RouteLine'
|
||||||
import { AssignBlock } from '../../components/Ticket'
|
import { AssignBlock, Ticket } from '../../components/Ticket'
|
||||||
import { Bar, Cross, MissMark, Ring, Tremble, Tri, Wave } from '../../components/shapes'
|
import { EntryGuide } from '../../components/EntryGuide'
|
||||||
|
import { Scatter } from '../../components/Scatter'
|
||||||
|
import { Cross, Ring, Tremble, Tri, Wave } from '../../components/shapes'
|
||||||
|
|
||||||
/* 场景坐标(复刻已验收组合):卡1(40,110,250) 卡2(380,44,250,rot) 卡3(700,44,140) */
|
/* 场景坐标(节点=卡描边真实边缘):卡1(40,110,250) 卡2(380,44,250,rot) 卡3(700,44,140) */
|
||||||
const nodes = {
|
const nodes = {
|
||||||
1: { id: 1, borderLeft: 40, right: 299, y: 180 },
|
1: { id: 1, borderLeft: 40, right: 290, y: 180 },
|
||||||
2: { id: 2, borderLeft: 378, right: 639, y: 108 },
|
2: { id: 2, borderLeft: 380, right: 630, y: 108 },
|
||||||
5: { id: 5, borderLeft: 698, right: 848, y: 108 },
|
5: { id: 5, borderLeft: 700, right: 840, y: 108 },
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
const SCATTER = [
|
|
||||||
{ el: <Tri size={26} />, x: 470, y: 14 },
|
|
||||||
{ el: <Ring size={22} />, x: 830, y: 150 },
|
|
||||||
{ el: <Cross size={20} />, x: 640, y: 440 },
|
|
||||||
{ el: <Cross size={18} />, x: 828, y: 218 },
|
|
||||||
{ el: <Wave size={44} />, x: 40, y: 316 },
|
|
||||||
{ el: <Bar size={30} />, x: 262, y: 548 },
|
|
||||||
{ el: <Tremble size={20} />, x: 96, y: 496 },
|
|
||||||
]
|
|
||||||
|
|
||||||
export function App() {
|
export function App() {
|
||||||
return (
|
return (
|
||||||
<Page tag="<Examples />" title="样例 · 组合场景" desc="全系统同台:拟物卡+三种关系线+指派块与脱靶标记+散落语义形状——实现验收的对照图。">
|
<Page
|
||||||
<div className="relative h-[620px] overflow-hidden rounded-2xl bg-paper">
|
tag="<Examples />"
|
||||||
<RouteLine edges={EDGES} nodes={nodes} width={1160} height={620} />
|
title="样例 · 组合场景"
|
||||||
{SCATTER.map((s, i) => (
|
desc="大部分组件同台真实使用:入口画像 → 课程卡与关系线 → 进度票券 → 指派块;形状散落于卡片附近空当。"
|
||||||
<div key={i} className="absolute" style={{ left: s.x, top: s.y }}>{s.el}</div>
|
>
|
||||||
))}
|
<EntryGuide />
|
||||||
|
<div className="relative mt-8 h-[600px] overflow-hidden rounded-2xl bg-paper">
|
||||||
|
<Scatter
|
||||||
|
shapes={[
|
||||||
|
<Tri size={26} key="tri" />,
|
||||||
|
<Ring size={22} key="ring" />,
|
||||||
|
<Cross size={20} key="cross1" />,
|
||||||
|
<Cross size={18} key="cross2" />,
|
||||||
|
<Wave size={44} key="wave" />,
|
||||||
|
<Tremble size={20} key="tremble" />,
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
<RouteLine edges={EDGES} nodes={nodes} width={1160} height={600} />
|
||||||
<div className="absolute left-[40px] top-[110px] w-[250px]"><MemphisCard chapter={CHAPTERS[1]} entry={1} /></div>
|
<div className="absolute left-[40px] top-[110px] w-[250px]"><MemphisCard chapter={CHAPTERS[1]} entry={1} /></div>
|
||||||
<div className="absolute left-[380px] top-[44px] w-[250px] -rotate-1"><MemphisCard chapter={CHAPTERS[2]} entry={2} /></div>
|
<div className="absolute left-[380px] top-[44px] w-[250px] -rotate-1"><MemphisCard chapter={CHAPTERS[2]} entry={2} /></div>
|
||||||
<div className="absolute left-[700px] top-[44px] w-[140px]"><MemphisCard chapter={CHAPTERS[5]} compact /></div>
|
<div className="absolute left-[700px] top-[44px] w-[140px]"><MemphisCard chapter={CHAPTERS[5]} compact /></div>
|
||||||
<div className="absolute left-[380px] top-[300px]"><AssignBlock {...ASSIGNMENT} /></div>
|
<div className="absolute left-[860px] top-[320px]"><AssignBlock {...ASSIGNMENT} /></div>
|
||||||
<div className="absolute left-[60px] top-[420px] w-[280px]">
|
<div className="absolute left-[60px] top-[440px] flex gap-4">
|
||||||
<div className="mb-2"><Wave size={40} /></div>
|
<Ticket chapter={CHAPTERS[2]} done />
|
||||||
<p className="m-0 text-[11px] text-body"><b>代理区</b>:安装交给 agent,只验收产物</p>
|
<Ticket chapter={CHAPTERS[5]} done={false} />
|
||||||
<div className="mt-3 flex items-center gap-2"><Bar size={30} /><span className="text-[11px] text-body"><b>价值区</b>:过程就地写清</span></div>
|
|
||||||
<div className="mt-6"><MissMark how="装了但没跑" looks={ASSIGNMENT.miss.looks} /></div>
|
|
||||||
</div>
|
</div>
|
||||||
<p className="absolute bottom-4 left-[40px] m-0 text-[10.5px] text-mute">
|
|
||||||
形状散落于空白,每个都有含义:▲ 靶子 · ● 积木 · ○ 预期 · ~ 代理区 · ▬ 价值区 · ⌇ 脱靶 · + 指派;线:实=先后 · 虚=相关 · 点=同族
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</Page>
|
</Page>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,21 +1,22 @@
|
|||||||
import { Page, Section } from '../../lib/ui'
|
import { Page, Section } from '../../lib/ui'
|
||||||
import { Bar, BrickDot, Cross, Ring, Tremble, Tri, Wave } from '../../components/shapes'
|
import { BrickDot, Cross, Mark, Ring, Tremble, Tri, Wave } from '../../components/shapes'
|
||||||
import { CHAPTERS } from '../../lib/chapters'
|
import { CHAPTERS } from '../../lib/chapters'
|
||||||
import { Ticket } from '../../components/Ticket'
|
import { Ticket } from '../../components/Ticket'
|
||||||
|
import type { ReactNode } from 'react'
|
||||||
|
|
||||||
const SHAPES = [
|
const SHAPES = [
|
||||||
{ el: <Tri size={28} />, n: '▲ 靶子', m: '目标,卡头打头', s: '靶子先行' },
|
{ el: <Tri size={28} />, n: '▲ 靶子', m: '目标,卡头打头', s: '靶子先行' },
|
||||||
{ el: <span className="flex"><BrickDot on /><BrickDot /></span>, n: '● 积木', m: '清单点列,实=已学', s: '积木清单' },
|
{ el: <span className="flex"><BrickDot on /><BrickDot /></span>, n: '● 积木', m: '清单点列,实=已学', s: '积木清单' },
|
||||||
{ el: <Ring size={28} />, n: '○ 预期', m: '可核对的结果', s: '预期结果' },
|
{ el: <Ring size={28} />, n: '○ 预期', m: '可核对的结果', s: '预期结果' },
|
||||||
{ el: <Wave size={36} />, n: '~ 代理区', m: '交给 agent', s: '每步先分类' },
|
{ el: <Wave size={36} />, n: '~ 代理区', m: '交给 agent', s: '每步先分类' },
|
||||||
{ el: <Bar size={30} />, n: '▬ 价值区', m: '就地写过程', s: '每步先分类' },
|
{ el: <Mark>过程</Mark>, n: '荧光标记 价值区', m: '划在文字上,不是散落图形', s: '每步先分类' },
|
||||||
{ el: <Tremble size={22} />, n: '⌇ 脱靶', m: '图形入口:追随图形进入阅读', s: '脱靶提醒' },
|
{ el: <Tremble size={22} />, n: '⌇ 脱靶', m: '图形入口:追随图形进入阅读', s: '脱靶提醒' },
|
||||||
{ el: <Cross size={22} />, n: '+ 指派', m: '位置/预期/场景', s: '指派学习' },
|
{ el: <Cross size={22} />, n: '+ 指派', m: '位置/预期/场景', s: '指派学习' },
|
||||||
]
|
]
|
||||||
|
|
||||||
const VOCAB: { p: string; rows: [string, string][] }[] = [
|
const VOCAB: { p: string; rows: [ReactNode, string][] }[] = [
|
||||||
{ p: '靶子', rows: [['▲ 目标', '做什么'], ['○ 预期结果', '做成长什么样、怎么核对'], ['● 积木清单', '要用到的零件——只列用到的;说零件不说知识']] },
|
{ p: '靶子', rows: [['▲ 目标', '做什么'], ['○ 预期结果', '做成长什么样、怎么核对'], ['● 积木清单', '要用到的零件——只列用到的;说零件不说知识']] },
|
||||||
{ p: '步骤分类', rows: [['~ 代理区', '无学习价值——交给 agent'], ['▬ 价值区', '有学习价值——就地写清']] },
|
{ p: '步骤分类', rows: [['~ 代理区', '无学习价值——交给 agent'], [<Mark key="v">价值区</Mark>, '有学习价值——就地写清']] },
|
||||||
{ p: '指派学习', rows: [['+ 三样(图形入口)', '教材只留三样,讲解交给 agent'], ['└ 位置', '知识体系中的坐标'], ['└ 预期', '实践结果应长什么样——当尺子'], ['└ 场景', '带回 agent 再做的练习——管巩固']] },
|
{ p: '指派学习', rows: [['+ 三样(图形入口)', '教材只留三样,讲解交给 agent'], ['└ 位置', '知识体系中的坐标'], ['└ 预期', '实践结果应长什么样——当尺子'], ['└ 场景', '带回 agent 再做的练习——管巩固']] },
|
||||||
{ p: '脱靶提醒', rows: [['⌇ 图形标记', '不人为挖坑;跟在指派后'], ['└ 可能性 / 长相', '怎么脱靶+脱靶什么样——撞上认出缺哪块']] },
|
{ p: '脱靶提醒', rows: [['⌇ 图形标记', '不人为挖坑;跟在指派后'], ['└ 可能性 / 长相', '怎么脱靶+脱靶什么样——撞上认出缺哪块']] },
|
||||||
{ p: '认知基础', rows: [['底座纹·真机验收章', 'env/库/运行环境——脚下的地,不是积木']] },
|
{ p: '认知基础', rows: [['底座纹·真机验收章', 'env/库/运行环境——脚下的地,不是积木']] },
|
||||||
@@ -30,7 +31,7 @@ const LINES = [
|
|||||||
export function App() {
|
export function App() {
|
||||||
return (
|
return (
|
||||||
<Page tag="<Symbols />" title="符号、形状 · 装饰即语义" desc="形状对内容下定义、散落于空白;线条形式承载关系含义;票券只表达完成状态。">
|
<Page tag="<Symbols />" title="符号、形状 · 装饰即语义" desc="形状对内容下定义、散落于空白;线条形式承载关系含义;票券只表达完成状态。">
|
||||||
<Section label="语义形状 ×7" note="散落只在空白,密度 ≤ 8/屏">
|
<Section label="语义形状 ×7" note="散落=卡片附近空当+随机旋转/落点(Scatter 种子化),密度 ≤ 8/屏">
|
||||||
<div className="grid grid-cols-4 gap-3.5 md:grid-cols-7">
|
<div className="grid grid-cols-4 gap-3.5 md:grid-cols-7">
|
||||||
{SHAPES.map((s) => (
|
{SHAPES.map((s) => (
|
||||||
<div key={s.n} className="text-center">
|
<div key={s.n} className="text-center">
|
||||||
|
|||||||
|
After Width: | Height: | Size: 182 KiB |
|
After Width: | Height: | Size: 155 KiB |
|
After Width: | Height: | Size: 135 KiB |
|
After Width: | Height: | Size: 187 KiB |
|
Before Width: | Height: | Size: 104 KiB |
|
Before Width: | Height: | Size: 176 KiB |
|
Before Width: | Height: | Size: 115 KiB |
|
Before Width: | Height: | Size: 179 KiB |