Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
8740795
🎨 Palette: [UX improvement] Add :active state to links
seonghobae Sep 2, 2026
c92d138
🎨 Palette: [UX 개선] 링크에 :active 상태 추가
seonghobae Sep 2, 2026
76d6022
🎨 Palette: [UX 개선] 링크에 :active 상태 추가
seonghobae Sep 2, 2026
d90be1c
test(ui): target visible link text for active feedback
seonghobae Sep 2, 2026
3e4f3bb
fix(ui): underline the visible entry name
seonghobae Sep 2, 2026
8ecb57a
🎨 Palette: [UX 개선] 링크에 :active 상태 추가
seonghobae Sep 3, 2026
25a4d26
🎨 Palette: [UX 개선] 링크에 :active 상태 추가
seonghobae Sep 3, 2026
2dda26f
🎨 Palette: [UX 개선] 링크에 :active 상태 추가
seonghobae Sep 3, 2026
d5e21ba
🎨 Palette: [UX 개선] 링크에 :active 상태 추가
seonghobae Sep 4, 2026
92eb0bc
🎨 Palette: [UX 개선] 링크에 :active 상태 추가
seonghobae Sep 4, 2026
36710b2
🎨 Palette: [UX improvement] BiDi 텍스트 지원을 위한 dir="auto" 속성 추가
seonghobae Sep 4, 2026
1f63c9e
🎨 Palette: [UX 개선] 링크에 :active 상태 추가
seonghobae Sep 5, 2026
dbc9917
fix(ui): restore semantic filename interaction target
seonghobae Sep 5, 2026
a1b9b98
🎨 Palette: [UX improvement] BiDi 텍스트 지원을 위한 dir="auto" 속성 추가
seonghobae Sep 5, 2026
31ddf8c
docs(palette): restore protected interaction guidance
seonghobae Sep 5, 2026
b811d47
merge(ui): restack filename interaction repair onto protected master
seonghobae Sep 5, 2026
0c2dbae
fix(i18n): restack BiDi name direction on semantic entry markup
seonghobae Sep 5, 2026
fc49489
docs(i18n): correct APA 7 retrieval metadata for living references
seonghobae Sep 5, 2026
a018891
🎨 Palette: [UX improvement] BiDi 텍스트 지원을 위한 dir="auto" 속성 추가
seonghobae Sep 5, 2026
e47f591
🎨 Palette: [UX improvement] BiDi 텍스트 지원을 위한 dir="auto" 속성 추가
seonghobae Sep 5, 2026
6d4055c
🎨 Palette: [UX improvement] BiDi 텍스트 지원을 위한 dir="auto" 속성 추가
seonghobae Sep 5, 2026
d73bffa
🎨 Palette: [UX improvement] BiDi 텍스트 지원을 위한 dir="auto" 속성 추가
seonghobae Sep 6, 2026
72c1240
🎨 Palette: [UX improvement] BiDi 텍스트 지원을 위한 dir="auto" 속성 추가
seonghobae Sep 6, 2026
ac9c8b3
🎨 Palette: [UX improvement] BiDi 텍스트 지원을 위한 dir="auto" 속성 추가
seonghobae Sep 6, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 6 additions & 8 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,8 @@ All notable changes to this project are documented in this file.

### Changed

- Improve generated directory-index readability with adjacent-row separators,
explicit light and dark empty-state text colors, and text-only hover/focus
underlining while retaining the full interactive target's focus outline.
- 생성된 디렉토리 인덱스의 가독성을 향상하기 위해 인접한 항목 간 구분선, 명시적인 라이트 모드 및 다크 모드용 빈 상태(empty-state) 텍스트 색상, 그리고 전체 인터랙티브 요소의 포커스 윤곽선은 유지하면서 텍스트에만 호버(hover) 및 포커스(focus) 밑줄이 적용되도록 개선했습니다.
- 외부에서 제공되어 기본 텍스트 방향을 알 수 없는 디렉토리 및 파일 이름 텍스트에 `dir="auto"`를 적용하고, 고정된 상위 디렉토리 이동 링크와 타입 라벨 등은 페이지의 기본 한국어 텍스트 방향을 따르도록 유지했습니다.

### Fixed

Expand All @@ -27,9 +26,8 @@ All notable changes to this project are documented in this file.

- Add a real generated-file regression test that independently recomputes the
declared style hash from the emitted `<style>` text.
- Add generated-page regressions for row ordering, empty-state semantics, CSS
cascade ordering, reduced-motion retention, text-only decoration, and numeric
text/focus contrast thresholds.
- 행 정렬(row ordering), 빈 상태 시맨틱(empty-state semantics), CSS 계단식 정렬(cascade ordering), 모션 감소 설정 유지(reduced-motion retention), 텍스트 전용 꾸밈(text-only decoration) 및 숫자 형태의 텍스트/포커스 대비율 테스트 등 생성 페이지 검증을 위한 회귀 테스트를 추가했습니다.
- 아랍어와 라틴어가 혼합된 디렉토리 및 파일 이름을 사용한 양방향(BiDi) 텍스트 출력 회귀 테스트를 추가했으며, 고정된 상위 탐색 경로와 라벨에 대한 경계 검증도 포함했습니다.

### Documentation

Expand All @@ -38,5 +36,5 @@ All notable changes to this project are documented in this file.
Search Central reference in `docs/doctoring/robots-indexing-preference.md`.
- Record the CSP byte-identity decision, threat boundary, verification contract,
and current W3C Working Draft reference in `docs/doctoring`.
- Record the generated-index readability decision, WCAG 2.2 engineering basis,
contrast calculations, scope boundaries, and verification contract.
- 생성 인덱스의 가독성 향상 결정, WCAG 2.2 기준에 기반한 설계 원리, 대비율 계산 방식, 적용 범위 및 검증 방식을 기록했습니다.
- 외부에서 제공되어 방향을 알 수 없는 이름에 대한 `dir="auto"`의 적용 범위 및 휴리스틱 경계를 `docs/doctoring/bidirectional-generated-names.md`에 한국어로 기록했습니다.
36 changes: 36 additions & 0 deletions docs/doctoring/bidirectional-generated-names.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# 생성된 이름에 대한 양방향 텍스트(BiDi) 방향성

## 문제 및 범위

`html4tree`는 파일 시스템에서 제공받은 디렉토리 및 파일 이름을 한국어로 생성된 인덱스 파일에 출력합니다. 이때 문자열의 기본 방향성을 미리 알 수 없습니다. 따라서 고정된 페이지 방향성만으로는 아랍어, 히브리어, 또는 양방향 혼합 문자열이 포함된 파일명을 제대로 처리할 수 없습니다.

제품 적용 범위는 의도적으로 제한했습니다: HTML의 `dir="auto"` 속성은 동적으로 변경되는 디렉토리 제목과 눈에 보이는 파일명 요소(`span`)에만 적용됩니다. 상위 디렉토리로 이동하는 `..` 링크나 아이콘의 시맨틱 요소, 숨겨진 파일/디렉토리 타입 라벨, 빈 상태 표시 등의 고정된 텍스트 요소에는 기존 페이지의 기본 방향성을 유지합니다. 이러한 결정이 `dir="auto"`로 구두점이나 모든 복잡한 텍스트 방향 문제 완벽히 해결한다는 의미는 아닙니다.

## 결정

외부에서 동적으로 제공된 디렉토리 및 파일 이름 문자열을 감싸고 있는 요소에만 제한적으로 `dir="auto"`를 사용합니다. 인터랙션 스타일링 규칙에 의존하는 시맨틱한 `.entry-name` 타겟 클래스는 유지해야 하며, 방향성 속성 추가로 인해 마크업이 위치 선택자에 의존하는 퇴행적 형태로 바뀌어서는 안 됩니다.

WHATWG HTML 표준에서는 `auto` 상태를 요소의 내용을 바탕으로 첫 번째 강력한 문자의 휴리스틱을 사용하여 방향을 격리하여 결정하도록 정의합니다. W3C 국제화 가이드라인도 삽입된 텍스트가 다국어를 포함할 수 있고 미리 방향을 알 수 없을 때 `dir="auto"` 사용을 권장하면서, 모든 예외 상황을 처리하지는 못한다는 점을 언급하고 있습니다.

## 검증

`BidirectionalTextDirectionTest` 테스트는 아랍어/라틴어가 혼합된 디렉토리 및 파일명을 사용하여 실제 인덱스 파일을 생성하며 다음 사항을 요구합니다:

- 동적으로 생성된 `<h1>`에 `dir="auto"` 존재 확인;
- 화면에 표시되는 파일명인 `.entry-name` `span`에 `dir="auto"` 존재 확인;
- 고정된 상위 디렉토리 탐색(parent-navigation) 링크에는 `dir="auto"`가 적용되지 않았음을 확인;
- 고정된 시각적으로 숨겨진 파일 타입 라벨에는 `dir="auto"`가 적용되지 않았음을 확인.

기존에 존재하는 `.entry-name`의 호버(hover), 포커스(focus-visible), 액티브(active) 상태 스타일에 대한 렌더링 회귀 테스트 역시 정상 작동해야 합니다. 정확한 커밋(head)에 대한 브라우저 렌더링 및 접근성 트리의 증거는 소스 검사에서 추론된 것보다 상위의 증거로 취급됩니다.

## 대안 및 롤백

전체 문서 수준에서 `dir="rtl"` 또는 `dir="auto"`를 적용하는 방식은, 한국어 텍스트와 UI 요소가 이미 알려진 LTR 방향을 가지기 때문에 거부되었습니다. CSS로만 텍스트 방향을 제어하는 방식은 CSS를 사용할 수 없는 환경에서도 HTML 텍스트 방향 메타데이터가 의미를 가져야 하기 때문에 채택되지 않았습니다. 유니코드 텍스트 제어 문자를 사용하는 방식은 동적 텍스트가 이미 별도의 엘리먼트 경계를 가지고 있고 안 보이는 문자열이 추가되면 유지 보수 측면에서 검증이 어려워지기 때문에 채택되지 않았습니다.

향후 최신 브라우저 환경에서 제품의 접근성이 퇴보(regression)하는 증거가 발견된다면 추가된 두 개의 `dir="auto"` 속성과 관련된 회귀 테스트를 제거하는 롤백을 수행할 수 있습니다. 단, 시맨틱 인터랙션 타겟인 `.entry-name` 클래스는 구조적으로 완전히 독립적이므로 변경 없이 보존되어야 합니다.

## 참고 자료

WHATWG. (n.d.). *HTML standard: The `dir` attribute*. Retrieved September 5, 2026, from https://html.spec.whatwg.org/multipage/dom.html#the-dir-attribute

W3C Internationalization. (n.d.). *Structural markup and right-to-left text in HTML*. Retrieved September 5, 2026, from https://www.w3.org/International/questions/qa-html-dir
12 changes: 6 additions & 6 deletions src/main/kotlin/html4tree/main.kt
Original file line number Diff line number Diff line change
Expand Up @@ -51,12 +51,12 @@ a {
border-radius: 4px;
transition: background-color 0.2s ease, outline-color 0.2s ease;
}
a:hover, a:focus-visible {
a:hover, a:focus-visible, a:active {
background-color: #f6f8fa;
outline: 2px solid #0969da;
outline-offset: -2px;
}
a:hover span:last-child, a:focus-visible span:last-child {
a:hover .entry-name, a:focus-visible .entry-name, a:active .entry-name {
text-decoration: underline;
}
@media (prefers-reduced-motion: reduce) {
Expand Down Expand Up @@ -94,7 +94,7 @@ li + li {
a {
color: #58a6ff;
}
a:hover, a:focus-visible {
a:hover, a:focus-visible, a:active {
background-color: #161b22;
outline-color: #58a6ff;
}
Expand Down Expand Up @@ -426,10 +426,10 @@ fun process_dir(curr_dir: File, excludeSet: Set<String>? = null, dirFiles: Array
</head>
<body>
<main>
<h1>${directoryName.escapeHtml()}</h1>
<h1 dir="auto">${directoryName.escapeHtml()}</h1>
<nav aria-label="디렉토리 목록">
<ul role="list">
<li><a class="dir-link" href="./.." title="상위 디렉토리로 이동"><span class="icon" aria-hidden="true">&#x21B0;</span> <span aria-hidden="true">..</span> <span class="visually-hidden">상위 디렉토리로 이동</span></a></li>
<li><a class="dir-link" href="./.." title="상위 디렉토리로 이동"><span class="icon" aria-hidden="true">&#x21B0;</span> <span class="entry-name" aria-hidden="true">..</span> <span class="visually-hidden">상위 디렉토리로 이동</span></a></li>
"""

val index_middle = fun():String{
Expand Down Expand Up @@ -460,7 +460,7 @@ fun process_dir(curr_dir: File, excludeSet: Set<String>? = null, dirFiles: Array
val ariaLabel = "${fileName} ${if (isLinkedDirectory) { "디렉토리" } else { "파일" }}".escapeHtml()
val typeLabel = if (isLinkedDirectory) { "디렉토리" } else { "파일" }
val icon = if (isLinkedDirectory) { "&#128193;" } else { "&#128196;" }
l.append(""" <li><a class="dir-link" href="${encodedHref}" title="${ariaLabel}"><span class="icon" aria-hidden="true">${icon}</span> <span>${fileName.escapeHtml()}</span> <span class="visually-hidden">${typeLabel}</span></a></li>""")
l.append(""" <li><a class="dir-link" href="${encodedHref}" title="${ariaLabel}"><span class="icon" aria-hidden="true">${icon}</span> <span class="entry-name" dir="auto">${fileName.escapeHtml()}</span> <span class="visually-hidden">${typeLabel}</span></a></li>""")
l.append('\n')
}
}
Expand Down
50 changes: 50 additions & 0 deletions src/test/kotlin/html4tree/BidirectionalTextDirectionTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package html4tree

import org.junit.After
import org.junit.Before
import org.junit.Test
import java.io.File
import java.nio.file.Files
import kotlin.test.assertFalse
import kotlin.test.assertTrue

/** Generated-output contract for externally supplied text with unknown direction. */
class BidirectionalTextDirectionTest {
private lateinit var temporaryDirectory: File

@Before
fun createTemporaryDirectory() {
temporaryDirectory = Files.createTempDirectory("html4tree-bidi-").toFile()
}

@After
fun removeTemporaryDirectory() {
temporaryDirectory.deleteRecursively()
}

@Test
fun dynamicDirectoryAndFilenameUseAutoWithoutReclassifyingFixedCopy() {
val generatedDirectory = File(temporaryDirectory, "دليل-Guide").apply { mkdir() }
val linkedFile = File(generatedDirectory, "تقرير-report.txt").apply { writeText("report") }

process_dir(generatedDirectory, setOf("index.html"), arrayOf(linkedFile))

val html = File(generatedDirectory, "index.html").readText(Charsets.UTF_8)
assertTrue(html.contains("<h1 dir=\"auto\">دليل-Guide</h1>"))
assertTrue(html.contains("<span class=\"entry-name\" dir=\"auto\">تقرير-report.txt</span>"))

val parentRow = Regex("""<a class="dir-link" href="\./\.\."[\s\S]*?</a>""")
.find(html)
?.value
assertTrue(parentRow != null)
assertTrue(parentRow!!.contains("<span class=\"entry-name\" aria-hidden=\"true\">..</span>"))
assertFalse(parentRow.contains("dir=\"auto\""))

val fileRow = Regex("""<a class="dir-link"[^>]*>[\s\S]*?تقرير-report\.txt[\s\S]*?</a>""")
.find(html)
?.value
assertTrue(fileRow != null)
assertTrue(fileRow!!.contains("<span class=\"visually-hidden\">파일</span>"))
assertFalse(fileRow.contains("<span class=\"visually-hidden\" dir=\"auto\">"))
}
}
24 changes: 19 additions & 5 deletions src/test/kotlin/html4tree/GeneratedIndexReadabilityTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ class GeneratedIndexReadabilityTest {
)

val generatedHtml = generatedHtml()
val parentIndex = generatedHtml.indexOf("<span aria-hidden=\"true\">..</span>")
val parentIndex = generatedHtml.indexOf("<span class=\"entry-name\" aria-hidden=\"true\">..</span>")
val firstIndex = generatedHtml.indexOf("alpha.txt")
val middleIndex = generatedHtml.indexOf("middle.txt")
val lastIndex = generatedHtml.indexOf("zulu.txt")
Expand Down Expand Up @@ -133,11 +133,24 @@ class GeneratedIndexReadabilityTest {
}

@Test
fun hoverAndKeyboardFocusUnderlineOnlyLinkText() {
process_dir(temporaryDirectory, setOf("index.html"), emptyArray())
fun interactiveStatesUnderlineVisibleFilenameOnly() {
val linkedFile = File(temporaryDirectory, "visible-name.txt").apply { writeText("target") }
process_dir(temporaryDirectory, setOf("index.html"), arrayOf(linkedFile))

val html = generatedHtml()
val style = emittedStyle()
val completeTargetRule = Regex("""a:hover, a:focus-visible \{([\s\S]*?)\}""")
val linkedRow = Regex("""<a class="dir-link"[^>]*>([\s\S]*?visible-name\.txt[\s\S]*?)</a>""")
.find(html)
?.groupValues
?.get(1)
assertNotNull(linkedRow)
assertTrue(linkedRow.contains("<span class=\"entry-name\" dir=\"auto\">visible-name.txt</span>"))
assertTrue(linkedRow.contains("<span class=\"icon\" aria-hidden=\"true\">"))
assertTrue(linkedRow.contains("<span class=\"visually-hidden\">파일</span>"))
assertFalse(linkedRow.contains("class=\"icon entry-name\""))
assertFalse(linkedRow.contains("class=\"visually-hidden entry-name\""))

val completeTargetRule = Regex("""a:hover, a:focus-visible, a:active \{([\s\S]*?)\}""")
.find(style)
?.groupValues
?.get(1)
Expand All @@ -147,12 +160,13 @@ class GeneratedIndexReadabilityTest {
assertTrue(
style.contains(
"""
a:hover span:last-child, a:focus-visible span:last-child {
a:hover .entry-name, a:focus-visible .entry-name, a:active .entry-name {
text-decoration: underline;
}
""".trimIndent()
)
)
assertFalse(style.contains("span:last-child"))
assertTrue(style.contains("@media (prefers-reduced-motion: reduce)"))
}

Expand Down
2 changes: 1 addition & 1 deletion src/test/kotlin/html4tree/MainTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -943,7 +943,7 @@ class MainTest {
assertTrue(indexHtml.exists())
val content = indexHtml.readText()
assertTrue(content.contains("<title>Root - 디렉토리 목록</title>"))
assertTrue(content.contains("<h1>Root</h1>"))
assertTrue(content.contains("<h1 dir=\"auto\">Root</h1>"))
}

}
Loading