dataface 0.1.6.dev76__py3-none-any.whl → 0.1.6.dev82__py3-none-any.whl
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.
- dataface/agent_api/data_paths.py +6 -12
- dataface/agent_api/docs/yaml-reference.md +1 -1
- dataface/agent_api/files.py +17 -13
- dataface/agent_api/pack.py +2 -2
- dataface/agent_api/project_session.py +4 -0
- dataface/agent_api/validate.py +1 -1
- dataface/ai/context.py +1 -1
- dataface/ai/tools/__init__.py +5 -5
- dataface/cli/commands/schema.py +23 -24
- dataface/core/compile/models/style/theme.py +1 -2
- dataface/core/compile/sizing.py +41 -12
- dataface/core/compile/typography.py +23 -29
- dataface/core/defaults/themes/_base.yaml +6 -6
- dataface/core/defaults/themes/cream.yaml +9 -0
- dataface/core/defaults/themes/stark.yaml +7 -6
- dataface/core/fonts.py +13 -0
- dataface/core/project.py +4 -0
- dataface/core/render/faces.py +24 -35
- dataface/core/render/font_measurement.py +2 -15
- dataface/core/render/layout_sizing.py +3 -0
- dataface/core/render/svg_utils.py +16 -7
- dataface/core/serve/alias_index.py +15 -7
- dataface/core/serve/server.py +9 -24
- {dataface-0.1.6.dev76.dist-info → dataface-0.1.6.dev82.dist-info}/METADATA +1 -1
- {dataface-0.1.6.dev76.dist-info → dataface-0.1.6.dev82.dist-info}/RECORD +30 -30
- mdsvg/renderer.py +68 -25
- mdsvg/style.py +2 -2
- {dataface-0.1.6.dev76.dist-info → dataface-0.1.6.dev82.dist-info}/WHEEL +0 -0
- {dataface-0.1.6.dev76.dist-info → dataface-0.1.6.dev82.dist-info}/entry_points.txt +0 -0
- {dataface-0.1.6.dev76.dist-info → dataface-0.1.6.dev82.dist-info}/licenses/LICENSE +0 -0
dataface/agent_api/data_paths.py
CHANGED
|
@@ -25,6 +25,7 @@ from dataface.core.registered_views.data_urls import (
|
|
|
25
25
|
|
|
26
26
|
if TYPE_CHECKING:
|
|
27
27
|
from dataface.agent_api.schema import SchemaResponse
|
|
28
|
+
from dataface.core.project import Project
|
|
28
29
|
from dataface.core.serve.alias_index import AliasIndex
|
|
29
30
|
|
|
30
31
|
|
|
@@ -186,22 +187,15 @@ def data_paths_list(
|
|
|
186
187
|
return result
|
|
187
188
|
|
|
188
189
|
|
|
189
|
-
def build_alias_index_for_project(
|
|
190
|
-
"""Build an AliasIndex from a project
|
|
190
|
+
def build_alias_index_for_project(project: Project) -> AliasIndex:
|
|
191
|
+
"""Build an AliasIndex from a project.
|
|
191
192
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
starting a server.
|
|
193
|
+
Intended for CLI commands (dft schema --data-paths) that need alias lookup
|
|
194
|
+
without starting a server.
|
|
195
195
|
"""
|
|
196
196
|
from dataface.core.serve.alias_index import AliasIndex
|
|
197
197
|
|
|
198
|
-
|
|
199
|
-
faces_at_root = faces_dir.is_dir()
|
|
200
|
-
return AliasIndex.build(
|
|
201
|
-
project_dir=project_dir,
|
|
202
|
-
faces_dir=faces_dir,
|
|
203
|
-
faces_at_root=faces_at_root,
|
|
204
|
-
)
|
|
198
|
+
return AliasIndex.build(project, faces_at_root=project.faces_dir.is_dir())
|
|
205
199
|
|
|
206
200
|
|
|
207
201
|
def data_alias_errors_for_file(
|
|
@@ -1081,7 +1081,7 @@ Authored overlay for TitleStyle. Board and face titles.
|
|
|
1081
1081
|
| Field | Type | Optional | Description |
|
|
1082
1082
|
|-------|------|:--------:|-------------|
|
|
1083
1083
|
| `font` | [FontStyle](#fontstyle) | ✓ | Title font style overrides. |
|
|
1084
|
-
| `line_height` | float | ✓ | Line height multiplier for titles and markdown headings. Headings typically want a tighter multiplier than body prose
|
|
1084
|
+
| `line_height` | float | ✓ | Line height multiplier for titles and markdown headings. Headings typically want a tighter multiplier than body prose. |
|
|
1085
1085
|
| `sizes` | list[float] | ✓ | Font sizes for the H1–H6 heading ramp, indexed by ``face.level - 1``. Combined with ``width_offsets`` at render time to size titles responsively by card width. |
|
|
1086
1086
|
| `width_offsets` | [TitleWidthOffsetsStyle](#titlewidthoffsetsstyle) | ✓ | Additive level offsets by card width (tiny/narrow/medium/wide). Added to the title's base level before indexing ``sizes``. Consumed by chart_title_spec / face_title_spec. |
|
|
1087
1087
|
| `min_height` | float | ✓ | Minimum title row height in pixels. |
|
dataface/agent_api/files.py
CHANGED
|
@@ -16,6 +16,8 @@ from pathlib import Path
|
|
|
16
16
|
|
|
17
17
|
from pydantic import BaseModel, Field
|
|
18
18
|
|
|
19
|
+
from dataface.core.project import Project
|
|
20
|
+
|
|
19
21
|
# Cap grep/glob payloads — read_file returns full content (model must handle size).
|
|
20
22
|
_MAX_GREP_MATCHES = 200
|
|
21
23
|
_MAX_GLOB_MATCHES = 500
|
|
@@ -146,9 +148,9 @@ def _walk_project(root: Path) -> list[Path]:
|
|
|
146
148
|
return result
|
|
147
149
|
|
|
148
150
|
|
|
149
|
-
def read_file(path: str,
|
|
151
|
+
def read_file(path: str, project: Project) -> ReadFileResult:
|
|
150
152
|
try:
|
|
151
|
-
target = _resolve_within(
|
|
153
|
+
target = _resolve_within(project.root, path)
|
|
152
154
|
except ValueError as exc:
|
|
153
155
|
return ReadFileResult(success=False, path=path, error=str(exc))
|
|
154
156
|
if not target.is_file():
|
|
@@ -157,12 +159,14 @@ def read_file(path: str, project_dir: Path) -> ReadFileResult:
|
|
|
157
159
|
content = target.read_text(encoding="utf-8")
|
|
158
160
|
except (OSError, UnicodeDecodeError) as exc:
|
|
159
161
|
return ReadFileResult(success=False, path=path, error=str(exc))
|
|
160
|
-
return ReadFileResult(
|
|
162
|
+
return ReadFileResult(
|
|
163
|
+
success=True, path=_rel(project.root, target), content=content
|
|
164
|
+
)
|
|
161
165
|
|
|
162
166
|
|
|
163
|
-
def write_file(path: str, content: str,
|
|
167
|
+
def write_file(path: str, content: str, project: Project) -> WriteFileResult:
|
|
164
168
|
try:
|
|
165
|
-
target = _resolve_within(
|
|
169
|
+
target = _resolve_within(project.root, path)
|
|
166
170
|
except ValueError as exc:
|
|
167
171
|
return WriteFileResult(success=False, path=path, error=str(exc))
|
|
168
172
|
try:
|
|
@@ -172,16 +176,16 @@ def write_file(path: str, content: str, project_dir: Path) -> WriteFileResult:
|
|
|
172
176
|
return WriteFileResult(success=False, path=path, error=str(exc))
|
|
173
177
|
return WriteFileResult(
|
|
174
178
|
success=True,
|
|
175
|
-
path=_rel(
|
|
179
|
+
path=_rel(project.root, target),
|
|
176
180
|
bytes_written=len(content.encode("utf-8")),
|
|
177
181
|
)
|
|
178
182
|
|
|
179
183
|
|
|
180
184
|
def edit_file(
|
|
181
|
-
path: str, old_string: str, new_string: str,
|
|
185
|
+
path: str, old_string: str, new_string: str, project: Project
|
|
182
186
|
) -> EditFileResult:
|
|
183
187
|
try:
|
|
184
|
-
target = _resolve_within(
|
|
188
|
+
target = _resolve_within(project.root, path)
|
|
185
189
|
except ValueError as exc:
|
|
186
190
|
return EditFileResult(success=False, path=path, error=str(exc))
|
|
187
191
|
if not target.is_file():
|
|
@@ -205,11 +209,11 @@ def edit_file(
|
|
|
205
209
|
target.write_text(original.replace(old_string, new_string), encoding="utf-8")
|
|
206
210
|
except OSError as exc:
|
|
207
211
|
return EditFileResult(success=False, path=path, error=str(exc))
|
|
208
|
-
return EditFileResult(success=True, path=_rel(
|
|
212
|
+
return EditFileResult(success=True, path=_rel(project.root, target), replacements=1)
|
|
209
213
|
|
|
210
214
|
|
|
211
|
-
def glob_files(pattern: str,
|
|
212
|
-
root =
|
|
215
|
+
def glob_files(pattern: str, project: Project) -> GlobResult:
|
|
216
|
+
root = project.root.resolve()
|
|
213
217
|
matches: list[str] = []
|
|
214
218
|
try:
|
|
215
219
|
candidates = root.glob(pattern)
|
|
@@ -228,8 +232,8 @@ def glob_files(pattern: str, project_dir: Path) -> GlobResult:
|
|
|
228
232
|
return GlobResult(success=True, matches=matches)
|
|
229
233
|
|
|
230
234
|
|
|
231
|
-
def grep_files(pattern: str,
|
|
232
|
-
root =
|
|
235
|
+
def grep_files(pattern: str, project: Project, glob: str | None = None) -> GrepResult:
|
|
236
|
+
root = project.root.resolve()
|
|
233
237
|
matches: list[GrepMatch] = []
|
|
234
238
|
try:
|
|
235
239
|
candidates: list[Path] = (
|
dataface/agent_api/pack.py
CHANGED
|
@@ -446,8 +446,8 @@ def apply_proposal(
|
|
|
446
446
|
)
|
|
447
447
|
|
|
448
448
|
# Ensure base dirs exist
|
|
449
|
-
|
|
450
|
-
(project.
|
|
449
|
+
project.faces_dir.mkdir(exist_ok=True)
|
|
450
|
+
(project.faces_dir / "partials").mkdir(exist_ok=True)
|
|
451
451
|
|
|
452
452
|
# Write partials — skipped by validate_paths (_*.yml convention)
|
|
453
453
|
for partial_filename, partial_path, rel in partial_targets:
|
|
@@ -253,6 +253,10 @@ class ProjectSession:
|
|
|
253
253
|
def warnings_ignore(self) -> frozenset[str]:
|
|
254
254
|
return self.project.warnings_ignore
|
|
255
255
|
|
|
256
|
+
@property
|
|
257
|
+
def faces_dir(self) -> Path:
|
|
258
|
+
return self.project.faces_dir
|
|
259
|
+
|
|
256
260
|
@cached_property
|
|
257
261
|
def _relationship_context(self) -> RelationshipContext | None:
|
|
258
262
|
"""Load relationship context from the super-schema cache (lazy, per-instance).
|
dataface/agent_api/validate.py
CHANGED
|
@@ -90,7 +90,7 @@ def _validate_one_path(
|
|
|
90
90
|
# detectors. Keep this lazy so `dft --help` doesn't pay that startup cost.
|
|
91
91
|
from dataface.core.inspect.manifest_utils import INSPECT_TEMPLATE_MANIFEST
|
|
92
92
|
|
|
93
|
-
raw_path = path if path is not None else project.
|
|
93
|
+
raw_path = path if path is not None else project.faces_dir
|
|
94
94
|
|
|
95
95
|
try:
|
|
96
96
|
resolved = resolve_scoped_path(raw_path, project.root)
|
dataface/ai/context.py
CHANGED
|
@@ -31,7 +31,7 @@ class DatafaceAIContext:
|
|
|
31
31
|
if path.is_absolute():
|
|
32
32
|
raise ValueError(f"Dashboard path must be relative: {path}")
|
|
33
33
|
if self.dashboards_directory is None:
|
|
34
|
-
base_dir =
|
|
34
|
+
base_dir = self.project_session.faces_dir.resolve()
|
|
35
35
|
else:
|
|
36
36
|
base_dir = self.dashboards_directory.resolve()
|
|
37
37
|
resolved = (base_dir / path).resolve()
|
dataface/ai/tools/__init__.py
CHANGED
|
@@ -293,14 +293,14 @@ def _handle_get_warning_code(
|
|
|
293
293
|
def _handle_read_file(args: dict[str, Any], ctx: DatafaceAIContext) -> dict[str, Any]:
|
|
294
294
|
parsed = _files.ReadFileArgs.model_validate(args)
|
|
295
295
|
return _files.read_file(
|
|
296
|
-
parsed.path,
|
|
296
|
+
parsed.path, project=Project(_render_project_dir(args, ctx))
|
|
297
297
|
).model_dump(mode="json", exclude_none=True)
|
|
298
298
|
|
|
299
299
|
|
|
300
300
|
def _handle_write_file(args: dict[str, Any], ctx: DatafaceAIContext) -> dict[str, Any]:
|
|
301
301
|
parsed = _files.WriteFileArgs.model_validate(args)
|
|
302
302
|
return _files.write_file(
|
|
303
|
-
parsed.path, parsed.content,
|
|
303
|
+
parsed.path, parsed.content, project=Project(_render_project_dir(args, ctx))
|
|
304
304
|
).model_dump(mode="json", exclude_none=True)
|
|
305
305
|
|
|
306
306
|
|
|
@@ -310,14 +310,14 @@ def _handle_edit_file(args: dict[str, Any], ctx: DatafaceAIContext) -> dict[str,
|
|
|
310
310
|
parsed.path,
|
|
311
311
|
parsed.old_string,
|
|
312
312
|
parsed.new_string,
|
|
313
|
-
|
|
313
|
+
project=Project(_render_project_dir(args, ctx)),
|
|
314
314
|
).model_dump(mode="json", exclude_none=True)
|
|
315
315
|
|
|
316
316
|
|
|
317
317
|
def _handle_glob_files(args: dict[str, Any], ctx: DatafaceAIContext) -> dict[str, Any]:
|
|
318
318
|
parsed = _files.GlobFilesArgs.model_validate(args)
|
|
319
319
|
return _files.glob_files(
|
|
320
|
-
parsed.pattern,
|
|
320
|
+
parsed.pattern, project=Project(_render_project_dir(args, ctx))
|
|
321
321
|
).model_dump(mode="json", exclude_none=True)
|
|
322
322
|
|
|
323
323
|
|
|
@@ -325,7 +325,7 @@ def _handle_grep_files(args: dict[str, Any], ctx: DatafaceAIContext) -> dict[str
|
|
|
325
325
|
parsed = _files.GrepFilesArgs.model_validate(args)
|
|
326
326
|
return _files.grep_files(
|
|
327
327
|
parsed.pattern,
|
|
328
|
-
|
|
328
|
+
project=Project(_render_project_dir(args, ctx)),
|
|
329
329
|
glob=parsed.glob,
|
|
330
330
|
).model_dump(mode="json", exclude_none=True)
|
|
331
331
|
|
dataface/cli/commands/schema.py
CHANGED
|
@@ -193,32 +193,31 @@ def schema_command(
|
|
|
193
193
|
lineage_depth=lineage_depth,
|
|
194
194
|
surface="cli",
|
|
195
195
|
)
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
196
|
+
if data_paths:
|
|
197
|
+
if not drill_result.success:
|
|
198
|
+
print_structured_errors(
|
|
199
|
+
drill_result.structured_errors
|
|
200
|
+
or [
|
|
201
|
+
StructuredError(
|
|
202
|
+
code=DF_UNKNOWN_INTERNAL.code,
|
|
203
|
+
domain=DF_UNKNOWN_INTERNAL.domain,
|
|
204
|
+
doc_url=DF_UNKNOWN_INTERNAL.doc_url,
|
|
205
|
+
docs_topic=DF_UNKNOWN_INTERNAL.docs_topic,
|
|
206
|
+
message="; ".join(drill_result.errors),
|
|
207
|
+
)
|
|
208
|
+
]
|
|
209
|
+
)
|
|
210
|
+
raise typer.Exit(1)
|
|
211
|
+
from dataface.agent_api.data_paths import (
|
|
212
|
+
build_alias_index_for_project,
|
|
213
|
+
data_paths_list,
|
|
210
214
|
)
|
|
211
|
-
raise typer.Exit(1)
|
|
212
|
-
from dataface.agent_api.data_paths import (
|
|
213
|
-
build_alias_index_for_project,
|
|
214
|
-
data_paths_list,
|
|
215
|
-
)
|
|
216
215
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
216
|
+
alias_index = build_alias_index_for_project(project_session.project)
|
|
217
|
+
ep_list = data_paths_list(drill_result, alias_index=alias_index)
|
|
218
|
+
wire = [p.to_dict() for p in ep_list]
|
|
219
|
+
typer.echo(json.dumps(wire, indent=2))
|
|
220
|
+
return
|
|
222
221
|
|
|
223
222
|
if json_output:
|
|
224
223
|
typer.echo(
|
|
@@ -332,8 +332,7 @@ class TitleStyle(BaseModel):
|
|
|
332
332
|
line_height: float = Field(
|
|
333
333
|
description=(
|
|
334
334
|
"Line height multiplier for titles and markdown headings. Headings "
|
|
335
|
-
"typically want a tighter multiplier than body prose
|
|
336
|
-
"the body 1.5-1.6)."
|
|
335
|
+
"typically want a tighter multiplier than body prose."
|
|
337
336
|
)
|
|
338
337
|
)
|
|
339
338
|
sizes: list[float] = Field(
|
dataface/core/compile/sizing.py
CHANGED
|
@@ -77,6 +77,8 @@ from typing import TYPE_CHECKING, Any, Literal, Protocol
|
|
|
77
77
|
|
|
78
78
|
from PIL import ImageFont
|
|
79
79
|
|
|
80
|
+
from dataface.core.compile.typography import face_is_prose
|
|
81
|
+
|
|
80
82
|
if TYPE_CHECKING:
|
|
81
83
|
from dataface.core.compile.models.face.resolved import (
|
|
82
84
|
ResolvedLayoutItem,
|
|
@@ -391,14 +393,15 @@ def compact_style_kwargs(
|
|
|
391
393
|
color comes from ``style.title.font.color``.
|
|
392
394
|
|
|
393
395
|
All prose colors derive from resolved theme tokens — no hardcoded table.
|
|
396
|
+
Paragraph spacing is derived from the body rhythm:
|
|
397
|
+
``font_size * line_height * 0.5``.
|
|
394
398
|
|
|
395
399
|
Args:
|
|
396
400
|
resolved_style: Optional ResolvedStyle for color resolution. Falls back to
|
|
397
401
|
the global config default when None.
|
|
398
402
|
heading_font_weight: Override the heading font weight. Face titles pass
|
|
399
403
|
the tier-specific weight (600 narrow, 500 medium/wide).
|
|
400
|
-
font_family: Override the body font family.
|
|
401
|
-
``config.style.title.font.family`` to render prose in serif.
|
|
404
|
+
font_family: Override the body font family used by mdsvg.
|
|
402
405
|
h1_size: Override the h1 pixel size. Face titles pass the
|
|
403
406
|
width-resolved pixel size from ``face_title_spec`` so the rendered
|
|
404
407
|
h1 lands exactly on the responsive target. When ``None``, h1 uses
|
|
@@ -419,9 +422,9 @@ def compact_style_kwargs(
|
|
|
419
422
|
_text_font_weight = _rs.text.font.weight
|
|
420
423
|
assert _text_font_weight is not None, "style.text.font.weight must be configured"
|
|
421
424
|
|
|
422
|
-
|
|
423
|
-
assert
|
|
424
|
-
_default_family = apply_emoji_to_family(
|
|
425
|
+
_text_font_family = _rs.text.font.family
|
|
426
|
+
assert _text_font_family is not None, "style.text.font.family must be configured"
|
|
427
|
+
_default_family = apply_emoji_to_family(_text_font_family, _rs.emoji_mode)
|
|
425
428
|
|
|
426
429
|
title_sizes = _rs.title.sizes
|
|
427
430
|
if len(title_sizes) != 6:
|
|
@@ -454,7 +457,7 @@ def compact_style_kwargs(
|
|
|
454
457
|
"line_height": float(_rs.text.line_height),
|
|
455
458
|
"heading_margin_top": 0.3,
|
|
456
459
|
"heading_margin_bottom": 0.2,
|
|
457
|
-
"paragraph_spacing":
|
|
460
|
+
"paragraph_spacing": float(_text_font_size) * float(_rs.text.line_height) * 0.5,
|
|
458
461
|
"text_color": _rs.font.color,
|
|
459
462
|
"heading_color": _rs.title.font.color,
|
|
460
463
|
"link_color": _rs.accent,
|
|
@@ -511,6 +514,13 @@ def compact_style_kwargs(
|
|
|
511
514
|
return kwargs
|
|
512
515
|
|
|
513
516
|
|
|
517
|
+
def body_text_font_family(resolved_style: ResolvedStyle) -> str:
|
|
518
|
+
"""Return the resolved body markdown family."""
|
|
519
|
+
family = resolved_style.text.font.family
|
|
520
|
+
assert family is not None, "style.text.font.family must be configured"
|
|
521
|
+
return family
|
|
522
|
+
|
|
523
|
+
|
|
514
524
|
def greedy_column_fill(
|
|
515
525
|
block_heights: list[float],
|
|
516
526
|
n_cols: int,
|
|
@@ -631,15 +641,18 @@ def get_markdown_text_height(
|
|
|
631
641
|
return 0.0
|
|
632
642
|
|
|
633
643
|
from dataface.core.compile.jinja import resolve_jinja_template
|
|
634
|
-
from dataface.core.fonts import
|
|
644
|
+
from dataface.core.fonts import get_font_path, get_mono_font_path
|
|
635
645
|
from mdsvg import measure as measure_markdown
|
|
636
646
|
|
|
637
647
|
resolved_content = resolve_jinja_template(
|
|
638
648
|
text, variable_defaults or {}, strict=False
|
|
639
649
|
)
|
|
640
650
|
|
|
641
|
-
|
|
642
|
-
|
|
651
|
+
_rs = resolved_style or resolve_style(get_config().style)
|
|
652
|
+
font_family = body_text_font_family(_rs)
|
|
653
|
+
|
|
654
|
+
font_path = get_font_path(font_family)
|
|
655
|
+
style = get_compact_style(_rs)
|
|
643
656
|
|
|
644
657
|
if text_style is not None and text_style.column.has_overrides:
|
|
645
658
|
return columned_text_height_estimate(
|
|
@@ -667,6 +680,7 @@ def get_title_height(
|
|
|
667
680
|
level: int = 1,
|
|
668
681
|
*,
|
|
669
682
|
resolved_style: ResolvedStyle | None = None,
|
|
683
|
+
prose: bool = False,
|
|
670
684
|
) -> float:
|
|
671
685
|
"""Get the actual height needed for a title by measuring it.
|
|
672
686
|
|
|
@@ -679,6 +693,7 @@ def get_title_height(
|
|
|
679
693
|
width: Available width for the title (drives tier selection)
|
|
680
694
|
variable_defaults: Optional variable defaults for Jinja resolution
|
|
681
695
|
resolved_style: Optional resolved face style for title typography measurement
|
|
696
|
+
prose: When True, measure with the theme's prose-title family.
|
|
682
697
|
|
|
683
698
|
Returns:
|
|
684
699
|
Actual height in pixels for the rendered title
|
|
@@ -687,7 +702,7 @@ def get_title_height(
|
|
|
687
702
|
return 0.0
|
|
688
703
|
|
|
689
704
|
from dataface.core.compile.jinja import resolve_jinja_template
|
|
690
|
-
from dataface.core.fonts import
|
|
705
|
+
from dataface.core.fonts import get_font_path, get_mono_font_path
|
|
691
706
|
from mdsvg import measure as measure_markdown
|
|
692
707
|
|
|
693
708
|
# Resolve any Jinja templates using variable defaults
|
|
@@ -704,15 +719,23 @@ def get_title_height(
|
|
|
704
719
|
# Measure the markdown to get actual dimensions (using compact style).
|
|
705
720
|
# text_align is intentionally omitted: mdsvg measure() only computes
|
|
706
721
|
# line-wrapped height, which is independent of horizontal alignment.
|
|
707
|
-
|
|
722
|
+
_rs = resolved_style or resolve_style(get_config().style)
|
|
723
|
+
if prose:
|
|
724
|
+
font_family = _rs.title.font.family
|
|
725
|
+
assert font_family is not None, "style.title.font.family must be configured"
|
|
726
|
+
else:
|
|
727
|
+
font_family = body_text_font_family(_rs)
|
|
728
|
+
|
|
729
|
+
font_path = get_font_path(font_family)
|
|
708
730
|
size = measure_markdown(
|
|
709
731
|
markdown_title,
|
|
710
732
|
width=width,
|
|
711
733
|
padding=0.0,
|
|
712
734
|
style=get_compact_style(
|
|
713
|
-
|
|
735
|
+
_rs,
|
|
714
736
|
h1_size=h1_size,
|
|
715
737
|
heading_font_weight=heading_weight,
|
|
738
|
+
font_family=font_family if prose else None,
|
|
716
739
|
),
|
|
717
740
|
font_path=font_path,
|
|
718
741
|
mono_font_path=str(get_mono_font_path()),
|
|
@@ -1008,12 +1031,14 @@ def compute_title_variables_inline_band_height(
|
|
|
1008
1031
|
)
|
|
1009
1032
|
if not face.title:
|
|
1010
1033
|
return 0.0
|
|
1034
|
+
prose = face_is_prose(face.text)
|
|
1011
1035
|
title_h = max(
|
|
1012
1036
|
get_title_height(
|
|
1013
1037
|
face.title,
|
|
1014
1038
|
title_w,
|
|
1015
1039
|
variable_defaults,
|
|
1016
1040
|
resolved_style=face.resolved_style,
|
|
1041
|
+
prose=prose,
|
|
1017
1042
|
),
|
|
1018
1043
|
float(face.resolved_style.title.min_height),
|
|
1019
1044
|
)
|
|
@@ -1134,11 +1159,13 @@ def calculate_layout(face: Face) -> Face:
|
|
|
1134
1159
|
elif face.title:
|
|
1135
1160
|
card_pad = float(config.style.board.card_padding)
|
|
1136
1161
|
title_measure_width = max(content_width - 2 * card_pad, 0.0)
|
|
1162
|
+
prose = face_is_prose(face.text)
|
|
1137
1163
|
title_height = get_title_height(
|
|
1138
1164
|
face.title,
|
|
1139
1165
|
title_measure_width,
|
|
1140
1166
|
variable_defaults,
|
|
1141
1167
|
resolved_style=face.resolved_style,
|
|
1168
|
+
prose=prose,
|
|
1142
1169
|
)
|
|
1143
1170
|
container_height += title_height + effective_gap
|
|
1144
1171
|
|
|
@@ -1675,12 +1702,14 @@ def nested_face_sizing_context(
|
|
|
1675
1702
|
nested_face, content_width, variable_defaults
|
|
1676
1703
|
)
|
|
1677
1704
|
elif nested_face.title:
|
|
1705
|
+
prose = face_is_prose(nested_face.text)
|
|
1678
1706
|
title_height = max(
|
|
1679
1707
|
get_title_height(
|
|
1680
1708
|
nested_face.title,
|
|
1681
1709
|
inner,
|
|
1682
1710
|
variable_defaults,
|
|
1683
1711
|
resolved_style=nrs,
|
|
1712
|
+
prose=prose,
|
|
1684
1713
|
),
|
|
1685
1714
|
float(nested_face.resolved_style.title.min_height),
|
|
1686
1715
|
)
|
|
@@ -29,14 +29,15 @@ Font family rules (theme-driven; values below are per-theme):
|
|
|
29
29
|
every width. On the shipped ``default`` (and ``cream``) the title-slot
|
|
30
30
|
family is serif (Source Serif 4), so medium/wide chart titles render serif
|
|
31
31
|
while smaller cards stay sans for legibility.
|
|
32
|
-
- Prose content
|
|
33
|
-
|
|
32
|
+
- Prose content uses the theme's text stack. Prose titles use the theme's
|
|
33
|
+
title stack at any width instead of the chart-title narrow-card fallback.
|
|
34
|
+
Prose is detected via ``is_prose(text)`` - true when word count >= 100.
|
|
34
35
|
|
|
35
|
-
Page and section titles (face/board headers)
|
|
36
|
-
``style.font.family``
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
36
|
+
Page and section titles (face/board headers) use ``style.text.font.family`` in
|
|
37
|
+
normal faces and ``style.title.font.family`` in prose faces. Face titles clamp
|
|
38
|
+
the width used for offset computation to the narrow boundary (≥ 360px) — a face
|
|
39
|
+
header should never shrink below the narrow tier even on a narrow nested
|
|
40
|
+
container.
|
|
40
41
|
|
|
41
42
|
Chart labels (axis, legend, tick) are separately themed — not controlled here.
|
|
42
43
|
|
|
@@ -47,7 +48,7 @@ Usage::
|
|
|
47
48
|
font_size, weight, family = chart_title_spec(
|
|
48
49
|
width, level=face_level + 1, resolved_chart_style=resolved_style.charts
|
|
49
50
|
)
|
|
50
|
-
font_size, weight
|
|
51
|
+
font_size, weight = face_title_spec(width, level=face_level)
|
|
51
52
|
"""
|
|
52
53
|
|
|
53
54
|
from __future__ import annotations
|
|
@@ -143,7 +144,8 @@ def is_prose(text: str) -> bool:
|
|
|
143
144
|
"""Return True if *text* qualifies as prose.
|
|
144
145
|
|
|
145
146
|
Prose is defined as body text with at least ``_PROSE_WORD_THRESHOLD`` words.
|
|
146
|
-
Prose content
|
|
147
|
+
Prose content uses ``style.text.font.family``. Its surrounding title uses
|
|
148
|
+
``style.title.font.family`` at any card width.
|
|
147
149
|
|
|
148
150
|
Args:
|
|
149
151
|
text: Raw text content (Markdown or plain).
|
|
@@ -154,6 +156,11 @@ def is_prose(text: str) -> bool:
|
|
|
154
156
|
return len(text.split()) >= _PROSE_WORD_THRESHOLD
|
|
155
157
|
|
|
156
158
|
|
|
159
|
+
def face_is_prose(text: str | None) -> bool:
|
|
160
|
+
"""Return whether optional face text qualifies for prose title treatment."""
|
|
161
|
+
return is_prose(text) if text else False
|
|
162
|
+
|
|
163
|
+
|
|
157
164
|
def chart_title_spec(
|
|
158
165
|
width: float,
|
|
159
166
|
*,
|
|
@@ -198,8 +205,8 @@ def chart_title_spec(
|
|
|
198
205
|
``build_resolved_style(...).charts`` so face-local and
|
|
199
206
|
chart-local style patches are already merged in.
|
|
200
207
|
use_title_family: ``True`` forces the title-slot family even on
|
|
201
|
-
narrow/tiny cards (so prose-tagged titles use
|
|
202
|
-
|
|
208
|
+
narrow/tiny cards (so prose-tagged titles use the theme's
|
|
209
|
+
title family at every width). ``False``
|
|
203
210
|
forces the body family at every width. ``None`` uses the
|
|
204
211
|
width default (title-slot family at medium/wide, body family
|
|
205
212
|
at narrow/tiny).
|
|
@@ -233,19 +240,12 @@ def face_title_spec(
|
|
|
233
240
|
*,
|
|
234
241
|
level: int,
|
|
235
242
|
resolved_style: ResolvedStyle | None = None,
|
|
236
|
-
) -> tuple[int, int | str
|
|
237
|
-
"""Return ``(font_size, font_weight
|
|
243
|
+
) -> tuple[int, int | str]:
|
|
244
|
+
"""Return ``(font_size, font_weight)`` for a face/page title.
|
|
238
245
|
|
|
239
246
|
Same logic as ``chart_title_spec`` but:
|
|
240
247
|
- The width used for offset computation is clamped to ``_TINY_MAX`` — a page
|
|
241
248
|
header should not shrink below the narrow tier even on a narrow container.
|
|
242
|
-
- Always returns sans (Inter) regardless of width.
|
|
243
|
-
|
|
244
|
-
Note: callers that render prose faces override the returned family to serif
|
|
245
|
-
by passing ``font_family=config.style.title.font.family`` to
|
|
246
|
-
``get_compact_style``. The function itself is family-agnostic; the prose
|
|
247
|
-
exception lives in the render layer (see ``render_title`` in
|
|
248
|
-
``render/svg_utils.py``).
|
|
249
249
|
|
|
250
250
|
Args:
|
|
251
251
|
width: Pixel width of the face/board.
|
|
@@ -254,12 +254,9 @@ def face_title_spec(
|
|
|
254
254
|
should override global config.
|
|
255
255
|
|
|
256
256
|
Returns:
|
|
257
|
-
``(font_size_px, css_font_weight
|
|
257
|
+
``(font_size_px, css_font_weight)``
|
|
258
258
|
"""
|
|
259
259
|
from dataface.core.compile.config import get_config
|
|
260
|
-
from dataface.core.compile.models.style.resolved import (
|
|
261
|
-
apply_emoji_to_family,
|
|
262
|
-
)
|
|
263
260
|
|
|
264
261
|
cfg = get_config()
|
|
265
262
|
title_style = (
|
|
@@ -279,10 +276,7 @@ def face_title_spec(
|
|
|
279
276
|
font_size = int(sizes[effective_level - 1])
|
|
280
277
|
|
|
281
278
|
weight = _coerce_weight(title_style.font.weight or 500)
|
|
282
|
-
|
|
283
|
-
assert _root is not None, "style.font.family must be configured"
|
|
284
|
-
_family = apply_emoji_to_family(_root, cfg.style.font.emoji)
|
|
285
|
-
return font_size, weight, _family
|
|
279
|
+
return font_size, weight
|
|
286
280
|
|
|
287
281
|
|
|
288
282
|
def face_title_markdown(
|
|
@@ -310,7 +304,7 @@ def face_title_markdown(
|
|
|
310
304
|
``(markdown_string, h1_size, font_weight)`` — pass to
|
|
311
305
|
``get_compact_style(h1_size=…, heading_font_weight=…)``.
|
|
312
306
|
"""
|
|
313
|
-
font_size, weight
|
|
307
|
+
font_size, weight = face_title_spec(
|
|
314
308
|
width, level=level, resolved_style=resolved_style
|
|
315
309
|
)
|
|
316
310
|
return f"# {title}", float(font_size), weight
|
|
@@ -74,9 +74,9 @@ style:
|
|
|
74
74
|
size: 18
|
|
75
75
|
weight: 500
|
|
76
76
|
case: title
|
|
77
|
-
# Tight headline leading. Convention: larger type wants tighter line-height
|
|
78
|
-
#
|
|
79
|
-
#
|
|
77
|
+
# Tight headline leading. Convention: larger type wants tighter line-height.
|
|
78
|
+
# At ~24px the body 1.25 multiplier reads loose between wrapped title lines;
|
|
79
|
+
# 1.1 sits in the standard display-headline range.
|
|
80
80
|
line_height: 1.1
|
|
81
81
|
# H1–H6 font-size ramp, indexed by face.level - 1.
|
|
82
82
|
# Paired with width_offsets below — chart_title_spec computes
|
|
@@ -704,9 +704,9 @@ style:
|
|
|
704
704
|
text:
|
|
705
705
|
font:
|
|
706
706
|
size: *size_body
|
|
707
|
-
# Body leading
|
|
708
|
-
#
|
|
709
|
-
line_height: 1.
|
|
707
|
+
# Body leading for dense report prose. Paragraph spacing is derived from
|
|
708
|
+
# this rhythm in the mdsvg mapper, so the vertical system stays proportional.
|
|
709
|
+
line_height: 1.25
|
|
710
710
|
align: left
|
|
711
711
|
|
|
712
712
|
layout:
|
|
@@ -28,6 +28,15 @@ style:
|
|
|
28
28
|
text:
|
|
29
29
|
font:
|
|
30
30
|
color: dft-creams.ink
|
|
31
|
+
code:
|
|
32
|
+
background: dft-creams.surface-subtle
|
|
33
|
+
border:
|
|
34
|
+
color: dft-creams.border
|
|
35
|
+
blockquote:
|
|
36
|
+
font:
|
|
37
|
+
color: dft-creams.muted
|
|
38
|
+
border:
|
|
39
|
+
color: dft-creams.border
|
|
31
40
|
charts:
|
|
32
41
|
# bar.border.color now tracks theme.background via self-reference at the
|
|
33
42
|
# default-theme level (see stark.yaml), so the cream knockout
|
|
@@ -132,20 +132,21 @@ style:
|
|
|
132
132
|
font:
|
|
133
133
|
family: '''ui-monospace'', ''SFMono-Regular'', Menlo, Consolas, monospace'
|
|
134
134
|
color: *color_ink
|
|
135
|
-
|
|
135
|
+
size: 10
|
|
136
|
+
background: dft-grays.surface-subtle
|
|
136
137
|
border:
|
|
137
|
-
color:
|
|
138
|
+
color: dft-grays.border
|
|
138
139
|
width: 1
|
|
139
140
|
radius: 4
|
|
140
141
|
|
|
141
142
|
blockquote:
|
|
142
143
|
font:
|
|
143
|
-
color:
|
|
144
|
+
color: dft-grays.muted
|
|
144
145
|
style: italic
|
|
145
|
-
background: '
|
|
146
|
+
background: ''
|
|
146
147
|
border:
|
|
147
|
-
color:
|
|
148
|
-
width:
|
|
148
|
+
color: dft-grays.border
|
|
149
|
+
width: 2
|
|
149
150
|
radius: 0
|
|
150
151
|
|
|
151
152
|
variables:
|