Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,16 +96,18 @@ The hub is a static site built with semantic HTML, CSS, and Vanilla JavaScript E
│ └── pdf-lib/
└── tests/
├── ci-foundation.test.mjs
├── home-structure.test.mjs
├── image-to-pdf.test.mjs
├── pdf-merge-and-categories.test.mjs
├── pdf-split.test.mjs
├── pdf-metadata.test.mjs
├── typography-i18n-layout.test.mjs
└── run-all.mjs
```

`js/main.js` initializes shared theme and internationalization behavior. `js/theme.js` owns Light, Dark, and System selection, OS color-scheme observation, and persistence. `js/i18n.js` resolves all six supported languages from browser preferences, applies `data-i18n` bindings without reloading, updates metadata and `<html lang>`, and gives persisted manual selection precedence. Repository links are centralized in `js/config.js`.

The concise [UX consistency audit](./docs/ux-consistency-audit.md) records the shared interaction patterns, resolved drift, and intentional tool-specific exceptions established before v1.0.0 hardening.
The concise [UX consistency audit](./docs/ux-consistency-audit.md) records the shared interaction patterns, category-first homepage follow-up, multilingual typography policy, resolved drift, and intentional tool-specific exceptions established before v1.0.0 hardening.

The category hubs are:

Expand Down Expand Up @@ -261,7 +263,7 @@ Run the complete local and CI validation entry point with:
node tests/run-all.mjs
```

It checks JavaScript syntax and runs Images to PDF, PDF Merge, PDF Split, PDF Organizer, PDF to Images, PDF Metadata, category route, six-language catalog parity, locale detection and persistence, selector and layout safeguards, static resource, privacy/network, security-hardening, ZIP, and CI workflow regression coverage. PDF fixtures are generated deterministically during tests; CI never processes real user files.
It checks JavaScript syntax and runs Images to PDF, PDF Merge, PDF Split, PDF Organizer, PDF to Images, PDF Metadata, category-first homepage, system typography, CJK wrapping, long-copy layout, six-language catalog parity and placeholders, locale detection and persistence, static resource, privacy/network, security-hardening, ZIP, and CI workflow regression coverage. PDF fixtures are generated deterministically during tests; CI never processes real user files.

## Production security controls

Expand Down
48 changes: 38 additions & 10 deletions css/base.css
Original file line number Diff line number Diff line change
@@ -1,6 +1,21 @@
:root {
color-scheme: light;
--font-sans: Inter, ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", "Apple SD Gothic Neo", "Malgun Gothic", "Hiragino Kaku Gothic ProN", "Yu Gothic UI", Meiryo, sans-serif;
--font-sans: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
--font-size-xs: 0.75rem;
--font-size-sm: 0.875rem;
--font-size-base: 1rem;
--font-size-lg: 1.125rem;
--font-size-xl: 1.35rem;
--line-height-tight: 1;
--line-height-heading: 1.12;
--line-height-body: 1.65;
--line-height-control: 1.35;
--letter-spacing-heading: -0.035em;
--font-weight-normal: 400;
--font-weight-medium: 500;
--font-weight-semibold: 600;
--font-weight-bold: 700;
--font-weight-heavy: 800;
--bg-primary: #f7f7f5;
--bg-secondary: #ffffff;
--bg-subtle: #efefec;
Expand Down Expand Up @@ -62,33 +77,46 @@ body {
background: var(--bg-primary);
color: var(--text-primary);
font-family: var(--font-sans);
font-size: 1rem;
line-height: 1.65;
font-size: var(--font-size-base);
line-height: var(--line-height-body);
text-rendering: optimizeLegibility;
transition: background-color var(--transition-theme), color var(--transition-theme);
overflow-wrap: break-word;
word-break: normal;
}

body, button, select { font: inherit; }
button, select { color: inherit; }
body, button, input, select, textarea { font: inherit; }
button, input, select, textarea { color: inherit; }
a { color: inherit; text-decoration-thickness: 0.08em; text-underline-offset: 0.2em; }
img, svg { display: block; max-width: 100%; }
h1, h2, h3, p { margin-block-start: 0; }
h1, h2, h3 { line-height: 1.12; letter-spacing: -0.035em; }
h1, h2, h3 { line-height: var(--line-height-heading); letter-spacing: var(--letter-spacing-heading); }
h1 { font-size: clamp(2.65rem, 8vw, 5.8rem); }
h2 { font-size: clamp(2rem, 4vw, 3.25rem); }
html:lang(ko) body { word-break: keep-all; }
html:lang(ja) body { line-break: strict; }
h3 { font-size: 1.1rem; letter-spacing: -0.015em; }
h3 { font-size: var(--font-size-lg); letter-spacing: -0.015em; }

html:lang(ko) {
--font-sans: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", "Apple SD Gothic Neo", "Malgun Gothic", "Noto Sans KR", sans-serif;
--line-height-heading: 1.2;
--line-height-body: 1.7;
--letter-spacing-heading: -0.02em;
}
html:lang(ja) {
--font-sans: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", "Hiragino Sans", "Yu Gothic UI", "Yu Gothic", Meiryo, "Noto Sans JP", sans-serif;
--line-height-heading: 1.2;
--line-height-body: 1.7;
--letter-spacing-heading: -0.02em;
line-break: strict;
}
html:lang(ko) :is(h1, h2, h3, .eyebrow, .hero-description, .category-card p, .category-tool p, .tool-intro > p:last-child, .drop-zone h2, .drop-zone p, .section-heading > p, .button, .control-label, .status) { word-break: keep-all; }
::selection { background: var(--accent-soft); }
:focus-visible { outline: 3px solid var(--focus); outline-offset: 3px; }

.container { width: min(calc(100% - 2rem), var(--content-width)); margin-inline: auto; }
.reading-width { max-width: var(--reading-width); }
.section { padding-block: var(--space-8); }
.section--bordered { border-top: 1px solid var(--border); }
.eyebrow { margin-bottom: var(--space-4); color: var(--accent); font-size: 0.75rem; font-weight: 750; letter-spacing: 0.13em; text-transform: uppercase; }
.eyebrow { margin-bottom: var(--space-4); color: var(--accent); font-size: var(--font-size-xs); font-weight: var(--font-weight-bold); letter-spacing: 0.13em; line-height: var(--line-height-control); text-transform: uppercase; }
.muted { color: var(--text-secondary); }

.skip-link {
Expand Down
5 changes: 3 additions & 2 deletions css/categories.css
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,15 @@
.category-intro > div > p:last-child, .category-intro > p { max-width: 38rem; margin-bottom: 0; color: var(--text-secondary); }
.category-nav { border-bottom: 1px solid var(--border); background: var(--bg-subtle); }
.category-nav__inner { display: flex; gap: var(--space-2); padding-block: var(--space-3); overflow-x: auto; scrollbar-width: thin; }
.category-nav a { flex: 0 0 auto; padding: 0.45rem 0.75rem; border-radius: var(--radius-sm); color: var(--text-secondary); font-size: 0.82rem; font-weight: 700; text-decoration: none; }
.category-nav a { display: inline-flex; min-height: 2.75rem; flex: 0 0 auto; align-items: center; padding: 0.45rem 0.75rem; border-radius: var(--radius-sm); color: var(--text-secondary); font-size: 0.82rem; font-weight: var(--font-weight-bold); line-height: var(--line-height-control); text-decoration: none; }
.category-nav a:hover, .category-nav a[aria-current="page"] { background: var(--bg-secondary); color: var(--text-primary); }
.category-tool-list { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--space-4); margin: 0; padding: 0; list-style: none; }
.category-tool { display: flex; min-width: 0; min-height: 12rem; flex-direction: column; align-items: flex-start; padding: var(--space-5); color: inherit; text-decoration: none; }
.category-tool__top { display: flex; width: 100%; align-items: center; justify-content: space-between; gap: var(--space-4); margin-bottom: auto; }
.category-tool h2 { margin: var(--space-5) 0 var(--space-2); font-size: clamp(1.3rem, 3vw, 1.75rem); }
.category-tool > .status { margin-bottom: auto; }
.category-tool p { margin: 0; color: var(--text-secondary); }
.category-tool > * { min-width: 0; }
.category-tool p { margin: 0; color: var(--text-secondary); overflow-wrap: anywhere; }
a.category-tool { transition: border-color var(--transition-fast), transform var(--transition-fast); }
a.category-tool:hover { border-color: var(--border-strong); transform: translateY(-2px); }
.category-tool__arrow { color: var(--accent); font-size: 1.35rem; }
Expand Down
26 changes: 14 additions & 12 deletions css/components.css
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
.button {
display: inline-flex;
min-width: 0;
min-height: 2.75rem;
align-items: center;
justify-content: center;
gap: var(--space-2);
padding: 0.65rem 1rem;
border: 1px solid transparent;
border-radius: var(--radius-sm);
font-weight: 700;
line-height: 1.2;
font-weight: var(--font-weight-bold);
line-height: var(--line-height-control);
max-width: 100%;
text-align: center;
overflow-wrap: anywhere;
text-decoration: none;
cursor: pointer;
transition: background-color var(--transition-fast), border-color var(--transition-fast), color var(--transition-fast);
Expand All @@ -24,39 +26,42 @@

.control {
min-height: 2.5rem;
min-width: 0;
max-width: 7.5rem;
padding: 0.4rem 1.8rem 0.4rem 0.65rem;
border: 1px solid var(--border);
border-radius: var(--radius-sm);
background-color: var(--bg-secondary);
line-height: var(--line-height-control);
transition: border-color var(--transition-fast), background-color var(--transition-theme);
}

.control[data-language-select] { width: 9rem; max-width: 9rem; }

.control:hover:not(:disabled) { border-color: var(--border-strong); }
.control:disabled { border-color: var(--border); background: var(--bg-subtle); color: var(--status-muted); cursor: not-allowed; }
.control-label { display: inline-flex; min-width: 0; align-items: center; gap: var(--space-2); color: var(--text-secondary); font-size: 0.75rem; font-weight: 650; }
.control-label { display: inline-flex; min-width: 0; align-items: center; gap: var(--space-2); color: var(--text-secondary); font-size: var(--font-size-xs); font-weight: var(--font-weight-semibold); line-height: var(--line-height-control); }
.surface { border: 1px solid var(--border); border-radius: var(--radius-lg); background: var(--bg-secondary); box-shadow: var(--shadow-sm); }
.status { display: inline-flex; align-items: center; gap: var(--space-2); color: var(--status-muted); font-size: 0.7rem; font-weight: 700; letter-spacing: 0.04em; text-transform: uppercase; white-space: nowrap; }
.status { display: inline-flex; max-width: 100%; align-items: center; gap: var(--space-2); color: var(--status-muted); font-size: var(--font-size-xs); font-weight: var(--font-weight-bold); letter-spacing: 0.04em; line-height: var(--line-height-control); text-align: center; text-transform: uppercase; }
.status::before { width: 0.45rem; height: 0.45rem; border-radius: 50%; background: currentColor; content: ""; }
.status--available { color: var(--accent); }
.text-link { font-weight: 700; text-decoration-color: var(--border-strong); transition: color var(--transition-fast), text-decoration-color var(--transition-fast); }
.text-link { font-weight: var(--font-weight-bold); text-decoration-color: var(--border-strong); transition: color var(--transition-fast), text-decoration-color var(--transition-fast); }
.text-link:hover { color: var(--accent); text-decoration-color: currentColor; }
.icon { width: 1.1rem; height: 1.1rem; flex: 0 0 auto; }

.site-header { position: relative; z-index: 10; border-bottom: 1px solid var(--border); background: color-mix(in srgb, var(--bg-primary) 92%, transparent); }
.header-inner { display: flex; min-height: 4.5rem; align-items: center; gap: var(--space-6); }
.brand { display: inline-flex; align-items: center; gap: var(--space-3); font-weight: 780; letter-spacing: -0.025em; text-decoration: none; white-space: nowrap; }
.brand { display: inline-flex; align-items: center; gap: var(--space-3); font-weight: var(--font-weight-bold); letter-spacing: -0.025em; text-decoration: none; white-space: nowrap; }
.brand-mark { width: 0.75rem; height: 0.75rem; border: 3px solid var(--accent); border-radius: 50%; box-shadow: 0 0 0 4px var(--accent-soft); }
.site-nav { display: flex; min-width: 0; flex-wrap: wrap; align-items: center; gap: var(--space-5); margin-right: auto; }
.site-nav a, .site-footer nav a { color: var(--text-secondary); font-size: 0.875rem; font-weight: 650; text-decoration: none; }
.site-nav a, .site-footer nav a { display: inline-flex; min-width: 0; min-height: 2.75rem; align-items: center; color: var(--text-secondary); font-size: var(--font-size-sm); font-weight: var(--font-weight-semibold); line-height: var(--line-height-control); overflow-wrap: anywhere; text-decoration: none; }
.site-nav a:hover, .site-footer nav a:hover { color: var(--text-primary); }
.header-inner > *, .header-controls > *, .control-label > span { min-width: 0; }
.header-controls { display: flex; min-width: 0; flex: 0 1 auto; align-items: center; gap: var(--space-3); }

.site-footer { padding-block: var(--space-7); border-top: 1px solid var(--border); }
.footer-inner { display: flex; align-items: flex-start; justify-content: space-between; gap: var(--space-6); }
.footer-inner p { margin: var(--space-3) 0 0; color: var(--text-secondary); font-size: 0.875rem; }
.footer-inner p { margin: var(--space-3) 0 0; color: var(--text-secondary); font-size: var(--font-size-sm); }
.site-footer nav { display: flex; flex-wrap: wrap; gap: var(--space-5); }

@media (max-width: 68rem) {
Expand All @@ -69,10 +74,7 @@
.header-controls { width: 100%; align-items: stretch; flex-direction: column; margin-left: 0; }
.control-label { width: 100%; justify-content: space-between; }
.control, .control[data-language-select] { width: min(100%, 12rem); max-width: none; }
.site-nav { justify-content: space-between; gap: var(--space-3); }
.site-nav { justify-content: flex-start; gap: var(--space-2) var(--space-4); }
.site-nav a { font-size: 0.8rem; }
.footer-inner { flex-direction: column; }
}

html:lang(ko) .button, html:lang(ko) .control-label, html:lang(ko) .status { word-break: keep-all; }
html:lang(ja) .button, html:lang(ja) .control-label, html:lang(ja) .status { line-break: strict; }
Loading
Loading