ucode 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (174) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +72 -0
  3. data/Gemfile.lock +2 -2
  4. data/TODO.full/00-README.md +116 -0
  5. data/TODO.full/01-panglyph-vision.md +112 -0
  6. data/TODO.full/02-panglyph-repo-bootstrap.md +184 -0
  7. data/TODO.full/03-panglyph-font-builder.md +201 -0
  8. data/TODO.full/04-panglyph-publish-pipeline.md +126 -0
  9. data/TODO.full/05-ucode-0-1-1-release.md +139 -0
  10. data/TODO.full/06-fontisan-remove-audit.md +142 -0
  11. data/TODO.full/07-fontisan-remove-ucd.md +125 -0
  12. data/TODO.full/08-archive-private-bin-build.md +143 -0
  13. data/TODO.full/09-archive-public-structure.md +164 -0
  14. data/TODO.full/10-fontist-org-woff-glyphs.md +131 -0
  15. data/TODO.full/11-fontist-org-audit-coverage.md +140 -0
  16. data/TODO.full/12-implementation-order.md +216 -0
  17. data/TODO.full/13-fontisan-font-writer-api.md +189 -0
  18. data/TODO.full/14-fontisan-table-writers.md +66 -0
  19. data/TODO.full/15-panglyph-builder-real.md +82 -0
  20. data/TODO.full/16-archive-public-sync-workflows.md +167 -0
  21. data/TODO.full/17-fontist-org-font-picker.md +73 -0
  22. data/TODO.full/18-comprehensive-spec-coverage.md +64 -0
  23. data/TODO.full/19-ucode-0-1-2-patch.md +32 -0
  24. data/TODO.full/20-fontisan-0-2-23-release.md +52 -0
  25. data/TODO.new/00-README.md +30 -0
  26. data/TODO.new/23-universal-glyph-set-source-map.md +312 -0
  27. data/TODO.new/24-universal-glyph-set-build.md +189 -0
  28. data/TODO.new/25-font-audit-against-universal-set.md +195 -0
  29. data/TODO.new/26-missing-glyph-reporter.md +189 -0
  30. data/TODO.new/27-fontist-org-consumer-integration.md +200 -0
  31. data/TODO.new/28-implementation-order-update.md +187 -0
  32. data/TODO.new/29-universal-set-curation-uc17.md +312 -0
  33. data/TODO.new/30-tier1-font-acquisition.md +241 -0
  34. data/TODO.new/31-universal-set-production-build.md +205 -0
  35. data/TODO.new/32-uc17-coverage-matrix.md +165 -0
  36. data/TODO.new/33-specialist-font-acquisition-refresh.md +138 -0
  37. data/TODO.new/34-pillar2-content-stream-correlator.md +147 -0
  38. data/TODO.new/35-universal-set-production-run.md +160 -0
  39. data/TODO.new/36-per-font-coverage-audit.md +145 -0
  40. data/TODO.new/37-coverage-highlight-reporter.md +125 -0
  41. data/TODO.new/38-fontist-org-glyph-consumer.md +141 -0
  42. data/TODO.new/39-implementation-order-update-32-38.md +258 -0
  43. data/TODO.new/40-archive-private-uses-ucode-audit.md +124 -0
  44. data/TODO.new/41-ucode-unicode-archive-bridge.md +160 -0
  45. data/config/specialist_fonts.yml +102 -0
  46. data/config/unicode17_tier1_fonts.yml +42 -0
  47. data/config/unicode17_universal_glyph_set.yml +293 -0
  48. data/lib/ucode/audit/block_aggregator.rb +57 -29
  49. data/lib/ucode/audit/browser/face_page.rb +128 -0
  50. data/lib/ucode/audit/browser/glyph_panel.rb +124 -0
  51. data/lib/ucode/audit/browser/library_page.rb +74 -0
  52. data/lib/ucode/audit/browser/missing_glyph_page.rb +87 -0
  53. data/lib/ucode/audit/browser/template.rb +47 -0
  54. data/lib/ucode/audit/browser/templates/face.css +200 -0
  55. data/lib/ucode/audit/browser/templates/face.html.erb +41 -0
  56. data/lib/ucode/audit/browser/templates/face.js +298 -0
  57. data/lib/ucode/audit/browser/templates/library.css +119 -0
  58. data/lib/ucode/audit/browser/templates/library.html.erb +42 -0
  59. data/lib/ucode/audit/browser/templates/library.js +99 -0
  60. data/lib/ucode/audit/browser/templates/missing_glyph_page.css +119 -0
  61. data/lib/ucode/audit/browser/templates/missing_glyph_page.html.erb +58 -0
  62. data/lib/ucode/audit/browser/templates/missing_glyph_page.js +2 -0
  63. data/lib/ucode/audit/browser.rb +32 -0
  64. data/lib/ucode/audit/context.rb +27 -1
  65. data/lib/ucode/audit/coverage_reference.rb +103 -0
  66. data/lib/ucode/audit/differ.rb +121 -0
  67. data/lib/ucode/audit/emitter/block_emitter.rb +52 -0
  68. data/lib/ucode/audit/emitter/codepoint_emitter.rb +87 -0
  69. data/lib/ucode/audit/emitter/collection_emitter.rb +80 -0
  70. data/lib/ucode/audit/emitter/face_directory.rb +212 -0
  71. data/lib/ucode/audit/emitter/glyph_emitter.rb +48 -0
  72. data/lib/ucode/audit/emitter/index_emitter.rb +149 -0
  73. data/lib/ucode/audit/emitter/library_emitter.rb +96 -0
  74. data/lib/ucode/audit/emitter/paths.rb +312 -0
  75. data/lib/ucode/audit/emitter/plane_emitter.rb +29 -0
  76. data/lib/ucode/audit/emitter/script_emitter.rb +29 -0
  77. data/lib/ucode/audit/emitter.rb +29 -0
  78. data/lib/ucode/audit/extractors/aggregations.rb +31 -2
  79. data/lib/ucode/audit/face_auditor.rb +86 -0
  80. data/lib/ucode/audit/formatters/audit_diff_text.rb +112 -0
  81. data/lib/ucode/audit/formatters/audit_text.rb +411 -0
  82. data/lib/ucode/audit/formatters/color.rb +48 -0
  83. data/lib/ucode/audit/formatters/library_summary_text.rb +98 -0
  84. data/lib/ucode/audit/formatters/text_formatter.rb +83 -0
  85. data/lib/ucode/audit/formatters.rb +23 -0
  86. data/lib/ucode/audit/library_aggregator.rb +86 -0
  87. data/lib/ucode/audit/library_auditor.rb +105 -0
  88. data/lib/ucode/audit/release/emitter.rb +152 -0
  89. data/lib/ucode/audit/release/face_card.rb +93 -0
  90. data/lib/ucode/audit/release/formula_audits.rb +50 -0
  91. data/lib/ucode/audit/release/library_index_builder.rb +78 -0
  92. data/lib/ucode/audit/release/manifest_builder.rb +127 -0
  93. data/lib/ucode/audit/release.rb +42 -0
  94. data/lib/ucode/audit/ucd_only_reference.rb +81 -0
  95. data/lib/ucode/audit/universal_set_reference.rb +136 -0
  96. data/lib/ucode/audit.rb +31 -0
  97. data/lib/ucode/cli.rb +339 -33
  98. data/lib/ucode/commands/audit/browser_command.rb +82 -0
  99. data/lib/ucode/commands/audit/collection_command.rb +103 -0
  100. data/lib/ucode/commands/audit/compare_command.rb +188 -0
  101. data/lib/ucode/commands/audit/font_command.rb +140 -0
  102. data/lib/ucode/commands/audit/library_command.rb +87 -0
  103. data/lib/ucode/commands/audit/reference_builder.rb +64 -0
  104. data/lib/ucode/commands/audit.rb +20 -0
  105. data/lib/ucode/commands/block_feed.rb +73 -0
  106. data/lib/ucode/commands/canonical_build.rb +138 -0
  107. data/lib/ucode/commands/fetch.rb +37 -1
  108. data/lib/ucode/commands/release.rb +115 -0
  109. data/lib/ucode/commands/universal_set.rb +211 -0
  110. data/lib/ucode/commands.rb +5 -0
  111. data/lib/ucode/coordinator/indices.rb +11 -0
  112. data/lib/ucode/coordinator.rb +138 -5
  113. data/lib/ucode/error.rb +30 -2
  114. data/lib/ucode/fetch/font_fetcher/result.rb +39 -0
  115. data/lib/ucode/fetch/font_fetcher.rb +16 -0
  116. data/lib/ucode/fetch/specialist_font_fetcher.rb +280 -0
  117. data/lib/ucode/fetch.rb +7 -3
  118. data/lib/ucode/glyphs/real_fonts/cmap_cache.rb +74 -0
  119. data/lib/ucode/glyphs/real_fonts.rb +1 -0
  120. data/lib/ucode/glyphs/resolver.rb +62 -0
  121. data/lib/ucode/glyphs/source.rb +48 -0
  122. data/lib/ucode/glyphs/source_builder.rb +61 -0
  123. data/lib/ucode/glyphs/source_config/coverage_assertion.rb +79 -0
  124. data/lib/ucode/glyphs/source_config/gap_report.rb +54 -0
  125. data/lib/ucode/glyphs/source_config.rb +104 -0
  126. data/lib/ucode/glyphs/sources/pillar1_embedded_tounicode.rb +63 -0
  127. data/lib/ucode/glyphs/sources/pillar3_last_resort.rb +51 -0
  128. data/lib/ucode/glyphs/sources/tier1_real_font.rb +104 -0
  129. data/lib/ucode/glyphs/sources.rb +20 -0
  130. data/lib/ucode/glyphs/universal_set/builder.rb +161 -0
  131. data/lib/ucode/glyphs/universal_set/coverage_report.rb +139 -0
  132. data/lib/ucode/glyphs/universal_set/idempotency.rb +86 -0
  133. data/lib/ucode/glyphs/universal_set/manifest_accumulator.rb +195 -0
  134. data/lib/ucode/glyphs/universal_set/manifest_writer.rb +61 -0
  135. data/lib/ucode/glyphs/universal_set/pre_build_check.rb +197 -0
  136. data/lib/ucode/glyphs/universal_set/validator.rb +204 -0
  137. data/lib/ucode/glyphs/universal_set.rb +45 -0
  138. data/lib/ucode/glyphs.rb +6 -0
  139. data/lib/ucode/models/audit/baseline.rb +6 -0
  140. data/lib/ucode/models/audit/block_summary.rb +7 -0
  141. data/lib/ucode/models/audit/codepoint_provenance.rb +39 -0
  142. data/lib/ucode/models/audit/release_face.rb +42 -0
  143. data/lib/ucode/models/audit/release_formula.rb +33 -0
  144. data/lib/ucode/models/audit/release_manifest.rb +43 -0
  145. data/lib/ucode/models/audit/release_universal_set.rb +37 -0
  146. data/lib/ucode/models/audit.rb +9 -0
  147. data/lib/ucode/models/block.rb +2 -0
  148. data/lib/ucode/models/build_report.rb +109 -0
  149. data/lib/ucode/models/codepoint/glyph.rb +42 -0
  150. data/lib/ucode/models/codepoint.rb +3 -0
  151. data/lib/ucode/models/glyph_source.rb +86 -0
  152. data/lib/ucode/models/glyph_source_map.rb +138 -0
  153. data/lib/ucode/models/specialist_font.rb +70 -0
  154. data/lib/ucode/models/specialist_font_manifest.rb +48 -0
  155. data/lib/ucode/models/unihan_entry.rb +81 -9
  156. data/lib/ucode/models/unihan_field.rb +21 -0
  157. data/lib/ucode/models/universal_set_entry.rb +47 -0
  158. data/lib/ucode/models/universal_set_manifest.rb +78 -0
  159. data/lib/ucode/models/validation_report.rb +99 -0
  160. data/lib/ucode/models.rb +9 -0
  161. data/lib/ucode/parsers/named_sequences.rb +5 -5
  162. data/lib/ucode/parsers/unihan.rb +50 -19
  163. data/lib/ucode/repo/aggregate_writer.rb +34 -2
  164. data/lib/ucode/repo/block_feed_emitter.rb +153 -0
  165. data/lib/ucode/repo/build_report_accumulator.rb +138 -0
  166. data/lib/ucode/repo/build_report_writer.rb +46 -0
  167. data/lib/ucode/repo/build_validator.rb +229 -0
  168. data/lib/ucode/repo/codepoint_writer.rb +50 -1
  169. data/lib/ucode/repo/paths.rb +8 -0
  170. data/lib/ucode/repo.rb +4 -0
  171. data/lib/ucode/version.rb +1 -1
  172. data/schema/block-feed.output.schema.yml +134 -0
  173. metadata +143 -2
  174. data/ucode.gemspec +0 -56
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pathname"
4
+ require "json"
5
+
6
+ require "ucode/repo/atomic_writes"
7
+ require "ucode/audit/browser"
8
+ require "ucode/audit/browser/template"
9
+ require "ucode/audit/emitter/paths"
10
+ require "ucode/audit/emitter/library_emitter"
11
+
12
+ module Ucode
13
+ module Audit
14
+ module Browser
15
+ # Renders the library-level `index.html` — a card grid of all
16
+ # audited fonts in a library, with search/sort/filter controls.
17
+ #
18
+ # The page inlines the same JSON shape that
19
+ # {Emitter::LibraryEmitter} writes to `index.json`, plus inlined
20
+ # CSS and JS. Clicking a card navigates to that face's
21
+ # per-face browser (`<label>/index.html`).
22
+ #
23
+ # Self-contained: no external resources. The entire
24
+ # `output/font_audit/` tree is portable as a unit.
25
+ #
26
+ # Two construction modes — pass exactly one of:
27
+ # - `summary:` — a live {Models::Audit::LibrarySummary}. The
28
+ # JSON shape is derived via {Emitter::LibraryEmitter#build_index}.
29
+ # - `library_json:` — a pre-built JSON string of the library
30
+ # overview shape. Used by {Commands::AuditBrowserCommand}
31
+ # when regenerating HTML from an existing `index.json`.
32
+ class LibraryPage
33
+ include Ucode::Repo::AtomicWrites
34
+
35
+ # @param summary [Models::Audit::LibrarySummary, nil]
36
+ # @param library_json [String, nil] pre-built library overview JSON
37
+ def initialize(summary: nil, library_json: nil)
38
+ raise ArgumentError, "pass exactly one of summary: / library_json:" \
39
+ unless summary.nil? ^ library_json.nil?
40
+
41
+ @summary = summary
42
+ @library_json = library_json
43
+ end
44
+
45
+ # Write the rendered page to `<library_root>/index.html`.
46
+ # @param output_root [String, Pathname]
47
+ # @return [Boolean] true if written, false if skipped
48
+ def write(output_root)
49
+ path = Ucode::Audit::Emitter::Paths.library_html_path(output_root)
50
+ write_atomic(path, render)
51
+ end
52
+
53
+ # Render the page as a string.
54
+ # @return [String]
55
+ def render
56
+ Template.new(:library).render(
57
+ library_json: library_json,
58
+ page_title: "ucode audit library",
59
+ )
60
+ end
61
+
62
+ private
63
+
64
+ def library_json
65
+ @library_json ||=
66
+ begin
67
+ hash = Ucode::Audit::Emitter::LibraryEmitter.new.build_index(@summary)
68
+ JSON.generate(hash)
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pathname"
4
+
5
+ require "ucode/repo/atomic_writes"
6
+ require "ucode/audit/browser"
7
+ require "ucode/audit/browser/template"
8
+ require "ucode/audit/browser/glyph_panel"
9
+ require "ucode/audit/emitter/paths"
10
+
11
+ module Ucode
12
+ module Audit
13
+ module Browser
14
+ # Renders one standalone per-block HTML gallery of missing
15
+ # glyphs at `<face_dir>/missing/<BLOCK>.html` (TODO 26).
16
+ #
17
+ # Every touched block with at least one missing codepoint gets
18
+ # one page. The gallery is fully static — inlined CSS, no
19
+ # client-side fetch — so it works via `file://` and is what
20
+ # fontist.org can iframe or screenshot for the "what's missing"
21
+ # widget.
22
+ #
23
+ # SVGs are inlined from the co-located universal-set build
24
+ # (when {GlyphPanel#available?}). When the universal set is
25
+ # absent, the page renders codepoint IDs only — no errors, no
26
+ # broken-image placeholders.
27
+ #
28
+ # For very large missing-codepoint sets (CJK can be thousands),
29
+ # the page emits at most +page_size+ thumbnails inline; an
30
+ # overflow notice replaces the rest.
31
+ class MissingGlyphPage
32
+ include Ucode::Repo::AtomicWrites
33
+
34
+ DEFAULT_PAGE_SIZE = 500
35
+
36
+ # @param block_name [String] Unicode block name (verbatim)
37
+ # @param missing_codepoints [Array<Integer>] codepoints the
38
+ # font does not cover for this block
39
+ # @param glyph_panel [GlyphPanel] service that reads the
40
+ # universal-set manifest + glyphs dir
41
+ # @param page_size [Integer] max thumbnails emitted inline
42
+ def initialize(block_name:, missing_codepoints:, glyph_panel:,
43
+ page_size: DEFAULT_PAGE_SIZE)
44
+ @block_name = block_name
45
+ @missing_codepoints = missing_codepoints
46
+ @glyph_panel = glyph_panel
47
+ @page_size = page_size
48
+ end
49
+
50
+ # @param face_dir [String, Pathname]
51
+ # @return [Boolean] true if written, false if skipped (identical)
52
+ def write(face_dir)
53
+ path = Ucode::Audit::Emitter::Paths.missing_glyph_page_under(face_dir, @block_name)
54
+ write_atomic(path, render)
55
+ end
56
+
57
+ # @return [String] rendered HTML
58
+ def render
59
+ Template.new(:missing_glyph_page).render(
60
+ block_name: @block_name,
61
+ panels: panel_data,
62
+ visible_count: visible.size,
63
+ total_count: @missing_codepoints.size,
64
+ overflow_count: overflow_count,
65
+ universal_set_available: @glyph_panel.available?,
66
+ )
67
+ end
68
+
69
+ private
70
+
71
+ def panel_data
72
+ visible.take(@page_size).map { |cp| @glyph_panel.to_hash(cp) }
73
+ end
74
+
75
+ def visible
76
+ @missing_codepoints.sort
77
+ end
78
+
79
+ def overflow_count
80
+ return 0 if @missing_codepoints.size <= @page_size
81
+
82
+ @missing_codepoints.size - @page_size
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "erb"
4
+ require "pathname"
5
+
6
+ require "ucode/audit/browser"
7
+
8
+ module Ucode
9
+ module Audit
10
+ module Browser
11
+ # Minimal ERB wrapper for the browser templates.
12
+ #
13
+ # Reads an `.erb` template plus its sibling `.css` and `.js`
14
+ # assets from {Browser::TEMPLATE_DIR}, renders them with the
15
+ # supplied binding, and returns a complete HTML document.
16
+ #
17
+ # Templates are plain ERB — no partials, no layouts, no helpers
18
+ # beyond what the binding provides. The CSS/JS assets are
19
+ # inlined into the rendered HTML so the page is fully
20
+ # self-contained (no external requests).
21
+ class Template
22
+ # @param name [String, Symbol] template name without extension
23
+ # (e.g. `:face`, `:library`)
24
+ def initialize(name)
25
+ @name = name
26
+ end
27
+
28
+ # @param locals [Hash{Symbol=>Object}] variables exposed in the
29
+ # template binding
30
+ # @return [String] rendered HTML
31
+ def render(locals = {})
32
+ erb = ERB.new(read("#{@name}.html.erb"), trim_mode: "-")
33
+ erb.result_with_hash(locals.merge(
34
+ _css: read("#{@name}.css"),
35
+ _js: read("#{@name}.js"),
36
+ ))
37
+ end
38
+
39
+ private
40
+
41
+ def read(filename)
42
+ Browser.const_get(:TEMPLATE_DIR).join(filename).read
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,200 @@
1
+ :root {
2
+ --bg: #fafafa;
3
+ --fg: #1a1a1a;
4
+ --muted: #5a5a5a;
5
+ --card-bg: #ffffff;
6
+ --border: #d8d8d8;
7
+ --accent: #2962ff;
8
+ --coverage-complete: #1b5e20;
9
+ --coverage-strong: #66bb6a;
10
+ --coverage-weak: #fdd835;
11
+ --coverage-none: #c62828;
12
+ --coverage-na: #b0b0b0;
13
+ }
14
+
15
+ @media (prefers-color-scheme: dark) {
16
+ :root {
17
+ --bg: #1a1a1a;
18
+ --fg: #e8e8e8;
19
+ --muted: #b0b0b0;
20
+ --card-bg: #2a2a2a;
21
+ --border: #3a3a3a;
22
+ --accent: #82b1ff;
23
+ }
24
+ }
25
+
26
+ * { box-sizing: border-box; }
27
+ body {
28
+ margin: 0;
29
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
30
+ background: var(--bg);
31
+ color: var(--fg);
32
+ line-height: 1.5;
33
+ }
34
+
35
+ .page-header {
36
+ padding: 1.5rem 2rem;
37
+ border-bottom: 1px solid var(--border);
38
+ background: var(--card-bg);
39
+ }
40
+ .page-header h1 { margin: 0 0 0.25rem; font-size: 1.5rem; }
41
+ .tagline { margin: 0; color: var(--muted); font-size: 0.875rem; }
42
+
43
+ main { padding: 1.5rem 2rem; max-width: 1200px; margin: 0 auto; }
44
+
45
+ section { margin-bottom: 2rem; }
46
+
47
+ .hint {
48
+ background: #fff3cd; color: #664d03;
49
+ padding: 0.75rem 1rem; border-radius: 4px;
50
+ font-size: 0.875rem;
51
+ }
52
+ @media (prefers-color-scheme: dark) {
53
+ .hint { background: #4d3a00; color: #ffe082; }
54
+ }
55
+ .hidden { display: none; }
56
+ code { font-family: ui-monospace, "SF Mono", Menlo, monospace; }
57
+
58
+ .card {
59
+ background: var(--card-bg);
60
+ border: 1px solid var(--border);
61
+ border-radius: 6px;
62
+ padding: 1rem 1.25rem;
63
+ }
64
+
65
+ .identity-grid {
66
+ display: grid;
67
+ grid-template-columns: max-content 1fr;
68
+ gap: 0.4rem 1.5rem;
69
+ font-size: 0.875rem;
70
+ }
71
+ .identity-grid dt {
72
+ color: var(--muted);
73
+ font-weight: 500;
74
+ text-align: right;
75
+ }
76
+ .identity-grid dd { margin: 0; word-break: break-all; }
77
+
78
+ .totals {
79
+ display: grid;
80
+ grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
81
+ gap: 1rem;
82
+ margin-top: 1rem;
83
+ }
84
+ .total-cell {
85
+ background: var(--bg);
86
+ border: 1px solid var(--border);
87
+ border-radius: 4px;
88
+ padding: 0.75rem 1rem;
89
+ }
90
+ .total-cell .label { color: var(--muted); font-size: 0.75rem; }
91
+ .total-cell .value { font-size: 1.25rem; font-weight: 600; }
92
+
93
+ .plane-band {
94
+ display: flex;
95
+ align-items: center;
96
+ gap: 0.5rem;
97
+ margin: 0.25rem 0;
98
+ }
99
+ .plane-band .label {
100
+ width: 60px;
101
+ color: var(--muted);
102
+ font-size: 0.75rem;
103
+ text-align: right;
104
+ }
105
+ .plane-band .strip {
106
+ flex: 1;
107
+ height: 12px;
108
+ background: var(--coverage-na);
109
+ border-radius: 2px;
110
+ display: flex;
111
+ overflow: hidden;
112
+ }
113
+ .plane-band .strip span {
114
+ height: 100%;
115
+ }
116
+
117
+ .block-table {
118
+ width: 100%;
119
+ border-collapse: collapse;
120
+ font-size: 0.875rem;
121
+ }
122
+ .block-table th,
123
+ .block-table td {
124
+ text-align: left;
125
+ padding: 0.5rem 0.75rem;
126
+ border-bottom: 1px solid var(--border);
127
+ }
128
+ .block-table th {
129
+ background: var(--bg);
130
+ color: var(--muted);
131
+ font-weight: 500;
132
+ cursor: pointer;
133
+ user-select: none;
134
+ }
135
+ .block-table td.num { text-align: right; font-variant-numeric: tabular-nums; }
136
+ .block-table tr.block-row { cursor: pointer; }
137
+ .block-table tr.block-row:hover { background: rgba(41, 98, 255, 0.08); }
138
+
139
+ .status {
140
+ display: inline-block;
141
+ padding: 2px 8px;
142
+ border-radius: 12px;
143
+ font-size: 0.7rem;
144
+ font-weight: 600;
145
+ letter-spacing: 0.03em;
146
+ }
147
+ .status-COMPLETE { background: var(--coverage-complete); color: #fff; }
148
+ .status-PARTIAL { background: var(--coverage-weak); color: #4a3500; }
149
+ .status-UNCOVERED_ASSIGNED { background: var(--coverage-none); color: #fff; }
150
+ .status-NO_ASSIGNED_IN_BLOCK,
151
+ .status-OUTSIDE_BASELINE { background: var(--coverage-na); color: #fff; }
152
+
153
+ .cp-chips {
154
+ display: flex;
155
+ flex-wrap: wrap;
156
+ gap: 4px;
157
+ margin-top: 0.5rem;
158
+ }
159
+ .cp-chip {
160
+ background: var(--bg);
161
+ border: 1px solid var(--border);
162
+ border-radius: 3px;
163
+ padding: 2px 6px;
164
+ font-family: ui-monospace, "SF Mono", Menlo, monospace;
165
+ font-size: 0.7rem;
166
+ cursor: pointer;
167
+ }
168
+ .cp-chip:hover { background: var(--accent); color: #fff; }
169
+
170
+ .detail-panel {
171
+ background: var(--bg);
172
+ border: 1px solid var(--border);
173
+ border-radius: 4px;
174
+ padding: 1rem;
175
+ margin-top: 0.5rem;
176
+ }
177
+ .detail-panel dt { color: var(--muted); font-size: 0.75rem; }
178
+ .glyph-preview {
179
+ background: #fff;
180
+ border: 1px solid var(--border);
181
+ padding: 0.5rem;
182
+ margin-top: 0.5rem;
183
+ display: inline-block;
184
+ min-width: 120px;
185
+ min-height: 60px;
186
+ }
187
+ .glyph-preview svg { max-width: 240px; max-height: 120px; display: block; }
188
+ .universal-set-preview { display: block; }
189
+ .universal-set-preview .hint { margin: 0 0 0.5rem; }
190
+ @media (prefers-color-scheme: dark) {
191
+ .glyph-preview { background: #f0f0f0; }
192
+ }
193
+
194
+ .discrepancies-list {
195
+ list-style: disc;
196
+ padding-left: 1.5rem;
197
+ font-size: 0.875rem;
198
+ }
199
+ .discrepancies-list li { margin-bottom: 0.25rem; }
200
+ .discrepancies-list .kind { font-family: ui-monospace, monospace; color: var(--accent); }
@@ -0,0 +1,41 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title><%= page_title %> — ucode audit</title>
7
+ <style>
8
+ <%= _css %>
9
+ </style>
10
+ </head>
11
+ <body data-verbose="<%= verbose %>" data-with-glyphs="<%= with_glyphs %>"
12
+ data-universal-set-available="<%= universal_set["available"] == true %>"
13
+ data-universal-set-glyphs-dir="<%= universal_set["glyphs_dir"] || "" %>"
14
+ data-universal-set-manifest-path="<%= universal_set["manifest_path"] || "" %>">
15
+ <header class="page-header">
16
+ <h1><%= page_title %></h1>
17
+ <p class="tagline">ucode font audit — self-contained page</p>
18
+ </header>
19
+
20
+ <main id="audit" aria-live="polite">
21
+ <noscript>
22
+ <p class="hint">This page requires JavaScript for interactive block drilldown.
23
+ The overview below is read from inlined JSON and renders without JS.</p>
24
+ </noscript>
25
+ <p class="hint hidden" id="file-url-hint">
26
+ Open via a local server (e.g. <code>python3 -m http.server</code> in this
27
+ directory) for full functionality. Block detail and glyph fetches are
28
+ blocked under <code>file://</code>.
29
+ </p>
30
+ <section id="overview" aria-busy="true"></section>
31
+ <section id="blocks"></section>
32
+ <section id="detail" class="hidden"></section>
33
+ <section id="discrepancies" class="hidden"></section>
34
+ </main>
35
+
36
+ <script type="application/json" id="audit-overview"><%= overview_json %></script>
37
+ <script>
38
+ <%= _js %>
39
+ </script>
40
+ </body>
41
+ </html>