nextpytk 0.4.7__tar.gz → 0.4.8__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nextpytk
3
- Version: 0.4.7
3
+ Version: 0.4.8
4
4
  Summary: Accessible, declarative Tkinter applications from ordinary Python functions.
5
5
  Project-URL: Homepage, https://github.com/nishimotz/nextpytk
6
6
  Project-URL: Repository, https://github.com/nishimotz/nextpytk
@@ -858,6 +858,7 @@ uv run python examples/disk_usage_flat_async.py # ncdu-style viewer (async
858
858
  uv run python examples/paired_demo.py # side-by-side paired layout with y-scroll sync
859
859
  uv run python examples/swap_demo.py # dynamic region switching (Layout.target + @app.swap)
860
860
  uv run python examples/bottom_bar_demo.py # pinned bottom bar (section side="bottom")
861
+ uv run python examples/live_validation.py # live validation via Tcl-var trace ingest (ingest_trace=True)
861
862
  ```
862
863
 
863
864
  ---
@@ -880,6 +881,10 @@ uv run python examples/bottom_bar_demo.py # pinned bottom bar (section sid
880
881
  - **`CustomTkinter`**: Modern look via Canvas rendering. nextpytk takes the opposite approach: use native widgets and embed A11y from the start.
881
882
  - **`TkRouter`** (israel-dryer, author of ttkbootstrap): Declarative view routing with URL-style paths, animated transitions, and history stack. Complements nextpytk's `multiview` — routing vs widget composition.
882
883
 
884
+ ## Changelog
885
+
886
+ See [CHANGES.md](./CHANGES.md) for the full release history.
887
+
883
888
  ## License
884
889
 
885
890
  MIT
@@ -834,6 +834,7 @@ uv run python examples/disk_usage_flat_async.py # ncdu-style viewer (async
834
834
  uv run python examples/paired_demo.py # side-by-side paired layout with y-scroll sync
835
835
  uv run python examples/swap_demo.py # dynamic region switching (Layout.target + @app.swap)
836
836
  uv run python examples/bottom_bar_demo.py # pinned bottom bar (section side="bottom")
837
+ uv run python examples/live_validation.py # live validation via Tcl-var trace ingest (ingest_trace=True)
837
838
  ```
838
839
 
839
840
  ---
@@ -856,6 +857,10 @@ uv run python examples/bottom_bar_demo.py # pinned bottom bar (section sid
856
857
  - **`CustomTkinter`**: Modern look via Canvas rendering. nextpytk takes the opposite approach: use native widgets and embed A11y from the start.
857
858
  - **`TkRouter`** (israel-dryer, author of ttkbootstrap): Declarative view routing with URL-style paths, animated transitions, and history stack. Complements nextpytk's `multiview` — routing vs widget composition.
858
859
 
860
+ ## Changelog
861
+
862
+ See [CHANGES.md](./CHANGES.md) for the full release history.
863
+
859
864
  ## License
860
865
 
861
866
  MIT
@@ -0,0 +1,61 @@
1
+ # ROADMAP — nextpytk
2
+
3
+ > **Note:** Past release history lives in [CHANGES.md](./CHANGES.md).
4
+ > This file is the forward-looking plan (near-term, then v0.5.0, then longer term).
5
+
6
+ ## Near term (post-0.4.8)
7
+
8
+ ### A11y
9
+
10
+ - [ ] `@app.status` live-region behavior (ARIA `aria-live` equivalent / WCAG 4.1.3)
11
+ - [ ] Tk 9.1 `tk accessible set_acc_*` real-device verification (9.1b0 + NVDA)
12
+ - [ ] Role vocabulary mapping to Tk side
13
+ - [ ] Automatic `emit_selection_change` (leverage `apply_state` knowing which widgets changed)
14
+
15
+ ### Type hints
16
+
17
+ - [ ] Further TypedDict / Protocol coverage for decorator arguments
18
+ - [ ] Fluent API equivalent to `_GridBuilder.columnconfigure`
19
+
20
+ ### Widget expansion
21
+
22
+ - [ ] Per-widget `bind` for other widgets (`text`, `combobox`, `treeview`, `button`, etc.)
23
+ - [ ] `@app.filepicker` — `filedialog` wrapper (schema/tool naming still open)
24
+
25
+ ### Layout DSL
26
+
27
+ - [ ] CSS-grid-inspired layout API for 0.5.0: explicit `grid-template-columns` / `grid-template-rows` strings (e.g. `"1fr 2fr"`, `"auto 1fr"`), area-based placement, and gap tokens. Reduce misuse by making rows/columns and widget placement visually aligned in one declaration.
28
+ - [ ] Wire visual Tab order for `side="bottom"` sections via `_wire_tab_order` (see v0.4.6 note in CHANGES.md).
29
+
30
+ ### Agent / LLM integration
31
+
32
+ - [ ] Expose `schema()` as Function Calling definition (bind `sequence`, treeview column defs, etc.)
33
+ - [ ] `@agent_tool` integration (GUI operations as agent vocabulary)
34
+ - [ ] Surface `@app.filepicker` in `schema()` / tool vocabulary
35
+
36
+ ---
37
+
38
+ ## v0.5.0 — themes / chrome decoupling
39
+
40
+ - Remove deprecated `theme=True/False` support; `theme` becomes `str` only.
41
+ - Remove deprecated `EventSeq.DOUBLE_CLICK`; use `EventSeq.PRIMARY_DOUBLE_CLICK` or `DOUBLE_BUTTON_1`.
42
+ - Remove deprecated index-order plural methods `col_weights`/`row_weights`/`col_minsizes`/`row_minsizes` and `Layout.cluster()` alias.
43
+ - Decouple `Layout().header()` / `.status()` chrome from Kizashi-specific tokens so they render safely with any ttk theme (`"none"`, `"clam"`, etc.).
44
+ - Extract Kizashi as a standalone ttk theme package so it can be proposed upstream or reused independently of nextpytk.
45
+
46
+ ---
47
+
48
+ ## Longer term
49
+
50
+ ### ttk Style layer
51
+
52
+ - [ ] `Layout.style("my_button", background=..., font=...)` style definitions
53
+
54
+ ### Declarative components
55
+
56
+ - [ ] `@app.component` decorator (React-like reuse, replacing `Layout`)
57
+ - [ ] State type definitions and validation (no Pydantic)
58
+
59
+ ### Testing
60
+
61
+ - [ ] WidgetSpec unit tests
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "nextpytk"
7
- version = "0.4.7"
7
+ version = "0.4.8"
8
8
  description = "Accessible, declarative Tkinter applications from ordinary Python functions."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.13"
@@ -268,11 +268,17 @@ class TkApp:
268
268
  *,
269
269
  debug: bool = False,
270
270
  theme: bool | str = "kizashi",
271
+ ingest_trace: bool = False,
271
272
  ):
272
273
  self._title = title
273
274
  self._debug = debug
274
275
  self._theme = self._normalize_theme(theme)
275
276
  self._kizashi = self._theme == "kizashi"
277
+ # When True, a ``trace_add("write", ...)`` is installed on every
278
+ # registered Tcl variable. User edits (e.g. typing into an entry)
279
+ # are ingested back into ``state`` (kept as the single source of
280
+ # truth) and batched into a single sync pass at the next idle event.
281
+ self._ingest_trace = ingest_trace
276
282
  self._acc_supported: bool | None = None
277
283
  self._a11y_last_toggle: dict[str, str] = {}
278
284
  self._widgets: list[WidgetSpec] = []
@@ -280,6 +286,15 @@ class TkApp:
280
286
  self._root: tk.Tk | None = None
281
287
  self._tk_widgets: dict[str, tk.Widget] = {}
282
288
  self._tk_vars: dict[str, tk.Variable] = {}
289
+ # Re-entrancy guard: while the framework itself calls ``var.set``
290
+ # (from apply_state), a write trace must NOT re-ingest that value.
291
+ self._syncing_var_keys: set[str] = set()
292
+ # Keys whose Tcl var changed from a user edit, awaiting a batched flush.
293
+ self._pending_ingest: set[str] = set()
294
+ self._ingest_flush_job: str | None = None
295
+ # True while widgets are being built; build-time ``var.set`` calls
296
+ # (e.g. placeholders) must not be ingested into state.
297
+ self._building = False
283
298
  self._widget_masters: dict[str, tk.Misc] = {}
284
299
  self._row_pack_jobs: list[tuple[tk.Frame, Any]] = []
285
300
  self._grid_pack_jobs: list[tuple[tk.Frame, Any]] = []
@@ -358,6 +373,9 @@ class TkApp:
358
373
  self._text_scroll_sync.clear()
359
374
  self._text_set_hooks.clear()
360
375
  self._a11y_last_toggle.clear()
376
+ self._pending_ingest.clear()
377
+ self._ingest_flush_job = None
378
+ self._syncing_var_keys.clear()
361
379
  self._current_view = None
362
380
  self._current_stage = None
363
381
  self._swap_targets.clear()
@@ -1290,7 +1308,7 @@ class TkApp:
1290
1308
  layout, row_jobs, grid_jobs = jobs_by_view[view]
1291
1309
  layout.pack_children_for(self, row_jobs, grid_jobs)
1292
1310
  else:
1293
- self.pack_view_widgets(view, center_kinds=centered, fill="x", pady=2)
1311
+ self.pack_view_widgets(view, center_kinds=centered, fill="x", pady=t.SPACE[1])
1294
1312
 
1295
1313
  def _on_tab_changed(_event: tk.Event[tk.Misc] | None = None) -> None:
1296
1314
  current = nb.select()
@@ -1462,7 +1480,7 @@ class TkApp:
1462
1480
  layout, row_jobs, grid_jobs = jobs_by_stage[stage]
1463
1481
  layout.pack_children_for(self, row_jobs, grid_jobs)
1464
1482
  else:
1465
- self.pack_view_widgets(stage, center_kinds=centered, fill="x", pady=2)
1483
+ self.pack_view_widgets(stage, center_kinds=centered, fill="x", pady=t.SPACE[1])
1466
1484
 
1467
1485
  active = str(self._state.get(key, stages[0] if stages else ""))
1468
1486
  if active not in stages:
@@ -2609,7 +2627,7 @@ class TkApp:
2609
2627
  """
2610
2628
  width = options.get("width", 300)
2611
2629
  height = options.get("height", 200)
2612
- bg = options.get("bg", "#f0f0f0")
2630
+ bg = options.get("bg", t.SURFACE)
2613
2631
  description = options.get("description")
2614
2632
  items = options.get("items")
2615
2633
  takefocus = options.get("takefocus")
@@ -2658,15 +2676,21 @@ class TkApp:
2658
2676
  update_to_apply.pop(stage_key, None)
2659
2677
 
2660
2678
  self._state.update(update_to_apply)
2661
- for key, val in update_to_apply.items():
2662
- var = self._tk_vars.get(key)
2663
- if var is None:
2664
- continue
2665
- s = "" if val is None else str(val)
2666
- var.set(s)
2667
- w = self._tk_widgets.get(key)
2668
- if isinstance(w, (tk.Entry, ttk.Entry)):
2669
- self._apply_entry_value_after_state(key, w, var, s)
2679
+ # Mark the keys being written by the framework so their write traces
2680
+ # (if ingest_trace is on) do not loop the value back into state.
2681
+ self._syncing_var_keys.update(update_to_apply)
2682
+ try:
2683
+ for key, val in update_to_apply.items():
2684
+ var = self._tk_vars.get(key)
2685
+ if var is None:
2686
+ continue
2687
+ s = "" if val is None else str(val)
2688
+ var.set(s)
2689
+ w = self._tk_widgets.get(key)
2690
+ if isinstance(w, (tk.Entry, ttk.Entry)):
2691
+ self._apply_entry_value_after_state(key, w, var, s)
2692
+ finally:
2693
+ self._syncing_var_keys.difference_update(update_to_apply)
2670
2694
  if full:
2671
2695
  self._sync_widgets()
2672
2696
  else:
@@ -2691,6 +2715,61 @@ class TkApp:
2691
2715
  # Keep state in sync with the rendered stage.
2692
2716
  self._state[key] = new_stage
2693
2717
 
2718
+ def _register_var(self, key: str, var: tk.Variable) -> None:
2719
+ """Register a Tcl variable under *key*, installing an ingest trace.
2720
+
2721
+ When ``ingest_trace=True``, a ``write`` trace feeds ``var.get()`` back
2722
+ into ``state[key]`` so user edits (typing into an entry, toggling a
2723
+ checkbutton, dragging a scale) become first-class state transitions.
2724
+ Framework-originated writes (from ``apply_state``) are excluded via
2725
+ ``_syncing_var_keys`` so they do not loop back. Multiple edits are
2726
+ coalesced into one ``after_idle`` sync pass.
2727
+ """
2728
+ self._tk_vars[key] = var
2729
+ if self._ingest_trace:
2730
+ try:
2731
+ var.trace_add(
2732
+ "write",
2733
+ lambda _n, _i, _op, k=key: self._on_var_ingest(k),
2734
+ )
2735
+ except tk.TclError:
2736
+ # Variable may already be deleted; ingest is best-effort.
2737
+ pass
2738
+
2739
+ def _on_var_ingest(self, key: str) -> None:
2740
+ """Trace callback: a user edit changed ``var``; queue it for ingest."""
2741
+ # Skip writes that the framework itself issued from apply_state, and
2742
+ # any writes that happen while widgets are being built (placeholders).
2743
+ if key in self._syncing_var_keys or self._building:
2744
+ return
2745
+ self._pending_ingest.add(key)
2746
+ if self._root is None:
2747
+ return
2748
+ if self._ingest_flush_job is None:
2749
+ try:
2750
+ self._ingest_flush_job = self._root.after_idle(self._flush_ingest)
2751
+ except tk.TclError:
2752
+ self._ingest_flush_job = None
2753
+
2754
+ def _flush_ingest(self) -> None:
2755
+ """Flush queued user edits into ``state`` in one batched pass."""
2756
+ self._ingest_flush_job = None
2757
+ if not self._pending_ingest:
2758
+ return
2759
+ pending = self._pending_ingest
2760
+ self._pending_ingest = set()
2761
+ update: dict[str, Any] = {}
2762
+ for key in pending:
2763
+ var = self._tk_vars.get(key)
2764
+ if var is None:
2765
+ continue
2766
+ try:
2767
+ update[key] = var.get()
2768
+ except tk.TclError:
2769
+ continue
2770
+ if update:
2771
+ self._apply_state_dict(update, full=False)
2772
+
2694
2773
  def _known_state_keys(self) -> set[str]:
2695
2774
  """Return the set of state keys that have meaning in the app."""
2696
2775
  keys: set[str] = set()
@@ -3346,6 +3425,19 @@ class TkApp:
3346
3425
 
3347
3426
  self._first_focusable: tk.Widget | None = None
3348
3427
 
3428
+ self._building = True
3429
+ try:
3430
+ self._build_widgets_inner()
3431
+ finally:
3432
+ self._building = False
3433
+
3434
+ def _build_widgets_inner(self) -> None:
3435
+ """Create tkinter widgets from registered specs (inside build guard)."""
3436
+ if self._root is None:
3437
+ return
3438
+
3439
+ self._first_focusable: tk.Widget | None = None
3440
+
3349
3441
  for spec in self._widgets:
3350
3442
  if spec.kind == "paned":
3351
3443
  master = self._widget_masters.get(spec.name, self._root)
@@ -3746,7 +3838,7 @@ class TkApp:
3746
3838
  )
3747
3839
  w = ttk.Entry(master, textvariable=var, style=style)
3748
3840
  self._tk_widgets[spec.name] = w
3749
- self._tk_vars[spec.name] = var
3841
+ self._register_var(spec.name, var)
3750
3842
  for opt in ("show", "width", "state"):
3751
3843
  if opt in e:
3752
3844
  w.configure(**{opt: e[opt]})
@@ -3792,7 +3884,7 @@ class TkApp:
3792
3884
  # anchor is a ttk layout option (style.layout), not a constructor kwarg
3793
3885
  # for ttk.Checkbutton; left alignment is enforced by the Kizashi style.
3794
3886
  self._tk_widgets[spec.name] = w
3795
- self._tk_vars[key] = var
3887
+ self._register_var(key, var)
3796
3888
  if spec.on_update is not None:
3797
3889
  fn = spec.on_update
3798
3890
  w.configure(command=lambda s=spec, f=fn, v=var, k=key:
@@ -3803,7 +3895,7 @@ class TkApp:
3803
3895
  gk = e.get("group_key", "radio")
3804
3896
  val = e.get("rb_value", "")
3805
3897
  if gk not in self._tk_vars:
3806
- self._tk_vars[gk] = tk.StringVar(value="")
3898
+ self._register_var(gk, tk.StringVar(value=""))
3807
3899
  var = self._tk_vars[gk]
3808
3900
  # padding is owned by the theme style (44px min target size)
3809
3901
  style = "TRadiobutton"
@@ -3933,7 +4025,7 @@ class TkApp:
3933
4025
  length=e.get("length", 200 if orient_str == "horizontal" else 100),
3934
4026
  ) # type: ignore[arg-type]
3935
4027
  self._tk_widgets[spec.name] = w
3936
- self._tk_vars[key] = var
4028
+ self._register_var(key, var)
3937
4029
  self._state[key] = str(var.get())
3938
4030
  if spec.on_update is not None:
3939
4031
  fn = spec.on_update
@@ -3964,7 +4056,7 @@ class TkApp:
3964
4056
  kwargs["font"] = e["font"]
3965
4057
  w = ttk.Spinbox(master, textvariable=var, **kwargs)
3966
4058
  self._tk_widgets[spec.name] = w
3967
- self._tk_vars[key] = var
4059
+ self._register_var(key, var)
3968
4060
  if init_val:
3969
4061
  self._state[key] = init_val
3970
4062
  if spec.on_update is not None:
@@ -3990,7 +4082,7 @@ class TkApp:
3990
4082
  kwargs["font"] = e["font"]
3991
4083
  w = ttk.Combobox(master, **kwargs)
3992
4084
  self._tk_widgets[spec.name] = w
3993
- self._tk_vars[key] = var
4085
+ self._register_var(key, var)
3994
4086
  if init_val:
3995
4087
  self._state[key] = init_val
3996
4088
  if spec.on_update is not None:
@@ -4001,7 +4093,7 @@ class TkApp:
4001
4093
  def _build_listbox(self, spec: WidgetSpec, master: tk.Misc) -> None:
4002
4094
  e = spec.extras
4003
4095
  kwargs_lb: dict[str, Any] = {
4004
- "bg": t.SURFACE,
4096
+ "bg": t.BG,
4005
4097
  "fg": t.TEXT,
4006
4098
  "selectbackground": t.ACCENT_RAMP[200],
4007
4099
  "selectforeground": t.ACCENT_RAMP[700],
@@ -60,7 +60,7 @@ def apply_theme(root):
60
60
  # --- entries ------------------------------------------------------------
61
61
  style.configure(
62
62
  "TEntry",
63
- fieldbackground=t.SURFACE,
63
+ fieldbackground=t.BG,
64
64
  foreground=t.TEXT,
65
65
  bordercolor=t.DIVIDER,
66
66
  lightcolor=t.DIVIDER,
@@ -225,6 +225,11 @@ def apply_theme(root):
225
225
  font=t.font("body"),
226
226
  focuscolor=t.FOCUS,
227
227
  padding=_check_radio_padding,
228
+ # clam's radiobutton gap between indicator and label is driven by
229
+ # ``indicatormargin`` (indicatorpadding is ignored by clam). Widen the
230
+ # right margin to SPACE[2] (8px) to match TCheckbutton, so the symbol
231
+ # and label read as separate without crowding.
232
+ indicatormargin=(0, 0, t.SPACE[2], 0),
228
233
  )
229
234
  style.layout(
230
235
  "TRadiobutton",
@@ -252,20 +257,30 @@ def apply_theme(root):
252
257
  )
253
258
 
254
259
  # --- scale --------------------------------------------------------------
260
+ # "Card" thumb: page-ground (BG) fill with an ACCENT border. The border
261
+ # provides the non-text contrast needed to pick the thumb out of the
262
+ # SURFACE trough (~5.3:1, WCAG 1.4.11); the BG fill against the ACCENT
263
+ # border stays legible too (~5.9:1). Hover/pressed shift the border.
255
264
  style.configure(
256
265
  "TScale",
257
266
  background=t.BG,
258
267
  troughcolor=t.SURFACE,
259
- bordercolor=t.DIVIDER,
260
- lightcolor=t.DIVIDER,
261
- darkcolor=t.DIVIDER,
268
+ bordercolor=t.ACCENT,
269
+ lightcolor=t.ACCENT,
270
+ darkcolor=t.ACCENT,
262
271
  padding=(t.SPACE[1], t.SPACE[2]),
263
272
  )
273
+ style.map(
274
+ "TScale",
275
+ bordercolor=[("active", t.ACCENT_HOVER), ("pressed", t.ACCENT_PRESSED)],
276
+ lightcolor=[("active", t.ACCENT_HOVER), ("pressed", t.ACCENT_PRESSED)],
277
+ darkcolor=[("active", t.ACCENT_HOVER), ("pressed", t.ACCENT_PRESSED)],
278
+ )
264
279
 
265
280
  # --- spinbox ------------------------------------------------------------
266
281
  style.configure(
267
282
  "TSpinbox",
268
- fieldbackground=t.SURFACE,
283
+ fieldbackground=t.BG,
269
284
  foreground=t.TEXT,
270
285
  bordercolor=t.DIVIDER,
271
286
  lightcolor=t.DIVIDER,
@@ -287,7 +302,7 @@ def apply_theme(root):
287
302
  # --- combobox -----------------------------------------------------------
288
303
  style.configure(
289
304
  "TCombobox",
290
- fieldbackground=t.SURFACE,
305
+ fieldbackground=t.BG,
291
306
  foreground=t.TEXT,
292
307
  bordercolor=t.DIVIDER,
293
308
  lightcolor=t.DIVIDER,
@@ -304,7 +319,7 @@ def apply_theme(root):
304
319
  lightcolor=[("focus", t.FOCUS)],
305
320
  darkcolor=[("focus", t.FOCUS)],
306
321
  arrowcolor=[("active", t.ACCENT_HOVER), ("pressed", t.ACCENT_PRESSED)],
307
- fieldbackground=[("readonly", t.SURFACE), ("active", t.SURFACE)],
322
+ fieldbackground=[("readonly", t.BG), ("active", t.BG)],
308
323
  selectbackground=[("focus", t.ACCENT_RAMP[200])],
309
324
  selectforeground=[("focus", t.TEXT)],
310
325
  )
@@ -316,7 +331,7 @@ def apply_theme(root):
316
331
  # foreground so the current value stays readable.
317
332
  style.configure(
318
333
  "Readonly.TCombobox",
319
- fieldbackground=t.NEUTRAL[100],
334
+ fieldbackground=t.BG,
320
335
  foreground=t.TEXT,
321
336
  bordercolor=t.DIVIDER,
322
337
  lightcolor=t.DIVIDER,
@@ -333,7 +348,7 @@ def apply_theme(root):
333
348
  lightcolor=[("focus", t.FOCUS)],
334
349
  darkcolor=[("focus", t.FOCUS)],
335
350
  arrowcolor=[("active", t.ACCENT_HOVER), ("pressed", t.ACCENT_PRESSED)],
336
- fieldbackground=[("readonly", t.NEUTRAL[100]), ("active", t.NEUTRAL[100])],
351
+ fieldbackground=[("readonly", t.BG), ("active", t.BG)],
337
352
  selectbackground=[("focus", t.ACCENT_RAMP[300])],
338
353
  selectforeground=[("focus", t.TEXT)],
339
354
  foreground=[("readonly", t.TEXT), ("active", t.TEXT)],
@@ -355,18 +370,22 @@ def apply_theme(root):
355
370
  )
356
371
 
357
372
  # --- scrollbar ----------------------------------------------------------
373
+ # Thumb matches the scale thumb: BG fill with an ACCENT border on a
374
+ # SURFACE trough. The border gives the non-text contrast to pick the
375
+ # thumb out of the trough; hover/pressed shift the border accent ramp.
358
376
  style.configure(
359
377
  "TScrollbar",
360
- background=t.NEUTRAL[300],
378
+ background=t.BG,
361
379
  troughcolor=t.SURFACE,
362
- bordercolor=t.DIVIDER,
380
+ bordercolor=t.ACCENT,
363
381
  arrowcolor=t.TEXT,
364
382
  gripcount=0,
365
383
  borderwidth=0,
366
384
  )
367
385
  style.map(
368
386
  "TScrollbar",
369
- background=[("active", t.NEUTRAL[400]), ("pressed", t.NEUTRAL[500])],
387
+ bordercolor=[("active", t.ACCENT_HOVER), ("pressed", t.ACCENT_PRESSED)],
388
+ background=[("active", t.NEUTRAL[100]), ("pressed", t.NEUTRAL[200])],
370
389
  arrowcolor=[("active", t.TEXT), ("pressed", t.TEXT)],
371
390
  )
372
391
 
@@ -538,23 +557,35 @@ def field_row(parent, row, label_text, textvariable, *, label_width=None, entry_
538
557
 
539
558
  def window_header(parent, title, subtitle=None):
540
559
  """Standard top-of-window block: left-aligned title, optional muted
541
- subtitle, closed by a strong 2px rule. Replaces stacks of centered
542
- gray label boxes — hierarchy comes from type scale, not from boxes.
560
+ subtitle. Hierarchy comes from type scale and spacing, not from boxes
561
+ or divider lines (Kizashi design principle).
543
562
 
544
563
  Returns (title_label, subtitle_label_or_None).
545
564
  """
546
- title_lbl = tk.Label(parent, text=title, bg=t.BG, fg=t.TEXT,
565
+ header_bg = t.BG # "#faf8f4" — same as page background
566
+
567
+ # Title: Kizashi h4 with space-2 (16px) internal padding for consistent
568
+ # background coverage. No bottom margin — spacing comes from subtitle.
569
+ title_lbl = tk.Label(parent, text=title, bg=header_bg, fg=t.TEXT,
547
570
  font=t.font("h4"), anchor="w",
548
- bd=0, highlightthickness=0)
571
+ bd=0, highlightthickness=0,
572
+ padx=t.SPACE[2], pady=t.SPACE[2])
549
573
  title_lbl.pack(fill="x")
574
+
550
575
  sub_lbl = None
551
576
  if subtitle is not None:
552
- sub_lbl = tk.Label(parent, text=subtitle, bg=t.BG, fg=t.TEXT_MUTED,
577
+ # Kizashi small text with space-2 padding.
578
+ # Top margin space-2 (16px) creates the gap from title.
579
+ sub_lbl = tk.Label(parent, text=subtitle, bg=header_bg, fg=t.TEXT_MUTED,
553
580
  font=t.font("small"), anchor="w",
554
- bd=0, highlightthickness=0)
555
- sub_lbl.pack(fill="x", pady=(t.SPACE[1], 0))
556
- rule = tk.Frame(parent, height=2, bg=t.DIVIDER, bd=0, highlightthickness=0)
557
- rule.pack(fill="x", pady=(t.SPACE[3], t.SPACE[4]))
581
+ bd=0, highlightthickness=0,
582
+ padx=t.SPACE[2], pady=t.SPACE[2])
583
+ sub_lbl.pack(fill="x")
584
+
585
+ # Add space-4 (32px) below the header to separate from the first section
586
+ # (Kizashi section spacing principle).
587
+ tk.Frame(parent, height=t.SPACE[4], bg=header_bg).pack(fill="x")
588
+
558
589
  return title_lbl, sub_lbl
559
590
 
560
591
 
@@ -581,7 +612,7 @@ def data_list(parent, columns, height=12, selectmode: str = "browse"):
581
612
  else:
582
613
  tree.column(key, width=width, minwidth=width, anchor=anchor, stretch=False)
583
614
 
584
- tree.tag_configure("odd", background=t.NEUTRAL[100])
615
+ tree.tag_configure("odd", background=t.BG)
585
616
  tree.tag_configure("even", background=t.BG)
586
617
 
587
618
  sb = ttk.Scrollbar(container, orient="vertical", command=tree.yview)
@@ -605,17 +636,16 @@ def clear_rows(tree):
605
636
 
606
637
 
607
638
  def status_bar(root, textvariable=None, text=""):
608
- """Bottom status bar: a 2px rule, then a left-aligned muted line.
639
+ """Bottom status bar: a left-aligned muted line.
609
640
 
610
641
  Use for hints and state instead of centered label boxes.
642
+ Hierarchy comes from spacing and typography (Kizashi principle).
611
643
  Returns the label.
612
644
  """
613
645
  bar = tk.Frame(root, bg=t.BG, bd=0, highlightthickness=0)
614
646
  bar.pack(side="bottom", fill="x")
615
- rule = tk.Frame(bar, height=2, bg=t.DIVIDER, bd=0, highlightthickness=0)
616
- rule.pack(fill="x")
617
647
  lbl = tk.Label(bar, bg=t.BG, fg=t.TEXT_MUTED, font=t.font("small"),
618
- anchor="w", padx=t.SPACE[6], pady=t.SPACE[2],
648
+ anchor="w", padx=t.SPACE[2], pady=t.SPACE[2],
619
649
  bd=0, highlightthickness=0)
620
650
  if textvariable is not None:
621
651
  lbl.configure(textvariable=textvariable)
@@ -22,7 +22,7 @@ import tkinter.font as tkfont
22
22
  # Contrast pairs are regression-tested in tests/test_tokens.py
23
23
  # (WCAG 1.4.3 Contrast (Minimum), 1.4.11 Non-text Contrast).
24
24
  BG = "#faf8f4" # Background (page ground)
25
- SURFACE = "#f1eae0" # Surface (fields, panels)
25
+ SURFACE = "#f1ece3" # Surface (fields, panels) — Kizashi spec
26
26
  CARD = "#ffffff" # Card background (design uses pure white on BG)
27
27
  TEXT = "#302b24" # Text Primary
28
28
  TEXT_SECONDARY = "#4a4339" # Paragraph text on cards/sections
nextpytk-0.4.7/ROADMAP.md DELETED
@@ -1,426 +0,0 @@
1
- # ROADMAP — nextpytk
2
-
3
- ## Snapshot (v0.4.0)
4
-
5
- ### Implemented widgets (`@app.*`)
6
-
7
- | kind | Notes |
8
- |------|-------|
9
- | `label` / `status` / `message` | Display / word-wrap. `status` sets `role=status` metadata (not a live region yet) |
10
- | `button` / `entry` | ttk, `state=` support |
11
- | `checkbutton` / `radiobutton` / `scale` / `spinbox` | ttk |
12
- | `combobox` | ttk.Combobox (`readonly`, `values`) |
13
- | `menubar` | Window menubar with cascades / `enabled_if` |
14
- | `text` / `listbox` / `canvas` | tk (Text: tags, readonly, paired y-scroll sync) |
15
- | `treeview` | Multi-column (`show="headings"`) |
16
- | `paned` | `ttk.Panedwindow` + `with app.pane(...)` |
17
- | `progressbar` | `state[key]` / `{name}_running` for determinate/indeterminate |
18
- | `bind` | Global shortcuts (annotates matching button) |
19
-
20
- Infrastructure: `Layout` (pack/grid, `.header()` / `.status()` chrome), `@app.multiview`, `@app.stages` / `run(stages=..., tabposition=...)`, `run` / `run_async` / `spawn` / `@app.job`, `schema()`, `debug_layout()`, builder registry, a11y choke point `_apply_a11y`.
21
-
22
- Theme: `tokens` + `apply_theme` / layout helpers (contrast + 44px target-size regressions).
23
-
24
- PyPI: Trusted Publishing on `v*` tag push (`.github/workflows/publish.yml`).
25
-
26
- ### Examples
27
-
28
- - `disk_usage_flat_async.py` — async ncdu-style viewer (`run_async` + `spawn`)
29
- - `combobox_demo.py`, `menubar_demo.py`, `header_demo.py`, `text_sync_demo.py`
30
-
31
- ---
32
-
33
- ## Snapshot (v0.4.1) — post-0.4 fixes
34
-
35
- ### Text widget ergonomics
36
-
37
- - `app.text_widget(name)` returns the real `tk.Text` instance (the outer container is still available via `app.widget(name)`).
38
- - `app.text_get(name)` returns the current full contents.
39
- - `text` widgets now sync from state via `apply_state({name: ...})`, including read-only panes.
40
- - `_on_text_change()` now reads from the real `tk.Text` instead of the empty container frame.
41
- - A11y traits now attach to the real `tk.Text` rather than the container frame.
42
-
43
- ### Layout documentation
44
-
45
- - Horizontal splits are already supported via `app.paned(..., orient=Orient.HORIZONTAL)` and `Layout.paned(...)`; see `examples/paned_split.py` and `examples/text_sync_demo.py`.
46
- - `on_ready` is intended for post-build setup, not reparenting already-packed widgets. Use up-front `paned` / `grid` / `section` declarations for dynamic-looking layouts.
47
- - The reported layout issue was **not** a nextpytk bug: widgets were placed in row 0 but `row_weights(0, 1)` gave all vertical stretch to the empty row 1. The framework behaved as documented; the fix was on the application side to use `row_weight(0, 1)` or a `paned` layout. This motivated stronger README warnings and the deprecation of the index-order plural methods below.
48
-
49
- ### Deprecations in v0.4.1 (removal in v0.5.0)
50
-
51
- - `col_weights(*weights)`, `row_weights(*weights)`, `col_minsizes(*sizes)`, and `row_minsizes(*sizes)` on `_GridBuilder` are now deprecated. They assigned weights by index position, which was easy to misuse.
52
- - `LayoutBuilder.grid(col_weights=..., row_weights=...)` keyword arguments are also deprecated.
53
- - Migrate to the explicit singular API: `col_weight(col, weight)`, `row_weight(row, weight)`, `col_minsize(col, size)`, `row_minsize(row, size)`.
54
-
55
- ### Design system documentation (v0.4.1)
56
-
57
- - Added a "Design System" section to README covering `Fill`/`Sticky`/`Orient` typed constants, `SPACE` token usage, and theme helpers.
58
- - Documented the `SPACE` scale (`1:4, 2:8, 3:12, 4:16, 6:24, 8:32`) and the rule of using tokens instead of bare integers for padding/spacing.
59
- - Updated `examples/grid_temp.py` and `examples/widget_gallery.py` to use the singular weight/min size API and `SPACE` tokens.
60
-
61
- ### Widget-level font and padding (v0.4.1)
62
-
63
- - Added `font=` to `@app.button`, `@app.entry`, `@app.checkbutton`, `@app.radiobutton`, `@app.text`, `@app.spinbox`, `@app.listbox`, and `@app.combobox` (in addition to the existing `@app.label`).
64
- - Added `padding=` to `@app.entry` so users can declaratively increase visual height (`ttk.Entry` does not expose `height` directly).
65
- - For ttk widgets whose widget-level `configure()` rejects `-font` / `-padding` (Button, Entry, Checkbutton, Radiobutton), nextpytk now creates a derived `ttk.Style` per widget. The new style inherits the base style's layout and theme, applies only the requested overrides, and leaves other theme properties intact.
66
- - Updated README.md, README.ja.md, and `tests/test_api.py` to cover the new options.
67
-
68
- ### Theme API redesign (v0.4.1)
69
-
70
- - `TkApp(theme=...)` now accepts `bool | str`.
71
- - `"kizashi"` (default) applies the built-in Kizashi design system.
72
- - `"none"` leaves the platform default ttk theme untouched.
73
- - Any other string switches to that installed ttk theme without Kizashi overrides.
74
- - `theme=True` and `theme=False` are deprecated in v0.4.1 and will be removed in v0.5.0. Migrate to `"kizashi"` and `"none"`.
75
- - Updated `README.md`, `README.ja.md`, and `tests/test_api.py`.
76
-
77
- ### Deferred
78
-
79
- - `listbox` callback stays index-based by design (unifies with `treeview`, handles duplicate items, and avoids forcing the framework to reverse-map strings). Passing the display string would be a breaking change and is intentionally not pursued.
80
-
81
- ---
82
-
83
- ## Snapshot (v0.4.2) — event-sequence generalization + listbox items_key
84
-
85
- ### Problem
86
-
87
- `@app.bind(...)` accepts arbitrary Tk event strings, but the only typed constants lived under `nextpytk.types.ListboxEvent` (e.g. `ListboxEvent.RETURN`). Using a listbox-oriented name for a global shortcut or an entry-level Enter binding was confusing.
88
-
89
- ### Change
90
-
91
- - Added a generic `nextpytk.types.EventSeq` class with common event sequences: `RETURN`, `ESCAPE`, `BACKSPACE`, `DELETE`, `TAB`, `DOUBLE_BUTTON_1`, `DOUBLE_BUTTON_2`, `DOUBLE_BUTTON_3`, `LISTBOX_SELECT`.
92
- - Kept `ListboxEvent` as a backward-compatible subclass of `EventSeq` so existing code continues to work. Existing constants (`SELECT`, `RETURN`, `KEY_BACKSPACE`, `KEY_DELETE`) still resolve to the same string values. `DOUBLE_CLICK` is kept as a deprecated alias of `DOUBLE_BUTTON_1` and will be removed in v0.5.0.
93
- - Updated `EventSeqLike` / `ListboxEventLike` to share the same literal union.
94
- - Updated `app.py` docstrings for `@app.bind` and `@app.listbox` to reference `EventSeq` instead of `ListboxEvent`.
95
-
96
- ### listbox / combobox dynamic choices
97
-
98
- - Added `items_key=` option to `@app.listbox` for state-driven list contents, symmetric to `treeview`'s existing `rows_key=`.
99
- - Added `values_key=` option to `@app.combobox` for state-driven dropdown values, following the same widget-native naming principle.
100
- - With these keys, `apply_state({items_key: [...]})` / `apply_state({values_key: [...]})` refresh the widget choices while keeping the selection in `state[name]` / `state[key]`.
101
- - Static `items=` / `values=` remain available and still work as before. When both are provided, the dynamic key takes precedence at runtime.
102
- - Full (`apply_state`) and partial (`_apply_state_dict`) updates both refresh the affected widgets when the dynamic key is touched.
103
- - Selection is kept when it remains valid; if it becomes invalid (removed from values, out of range for listbox) it is reset to the widget's empty state.
104
-
105
- ### entry widget-level events
106
-
107
- - Added `events=` option to `@app.entry` for widget-level event bindings.
108
- - Accepts `dict[str, ListboxEventHandler]` mapping event sequences (e.g. `EventSeq.RETURN`) to handlers.
109
- - Handlers receive a dict of current effective entry values (same shape as button callbacks) and may return a state update dict.
110
- - Enables patterns like "press Enter in the search entry to trigger search" without a global `<Return>` bind or an application-level button.
111
-
112
- ### A11y-aware primary mouse-button helpers
113
-
114
- - Added `EventSeq.PRIMARY_CLICK`, `EventSeq.PRIMARY_DOUBLE_CLICK`, and `EventSeq.PRIMARY_BUTTON_RELEASE` lazy descriptors to `nextpytk.types`. These return the tkinter event sequence for the OS-configured primary mouse button, accounting for left/right button swap on Windows, macOS, and Linux/GNOME. (The underlying `primary_click()`, `primary_double_click()`, and `primary_button_release()` functions are also available for direct use.)
115
- - `_primary_button_number()` detects the swap setting per platform and caches the result with `functools.lru_cache(maxsize=1)` so repeated event lookups do not hit the OS each time.
116
- - `EventSeq.PRIMARY_CLICK`, `EventSeq.PRIMARY_DOUBLE_CLICK`, and `EventSeq.PRIMARY_BUTTON_RELEASE` are lazy descriptors that re-evaluate on each access (the underlying `_primary_button_number()` is cached, so the cost is negligible). These are the recommended way to reference primary-button events in widget-level bindings (e.g. `events={EventSeq.PRIMARY_DOUBLE_CLICK: ...}` on `@app.listbox`).
117
- - Updated `examples/disk_usage_flat_async.py` and the `@app.listbox` docstring to use `EventSeq.PRIMARY_DOUBLE_CLICK` instead of the deprecated `EventSeq.DOUBLE_CLICK`.
118
-
119
- ### Tests
120
-
121
- - Added `tests/test_types.py` covering `DOUBLE_BUTTON_*`, the `DOUBLE_CLICK` alias, default primary-button behavior, and mocked Windows/macOS/GNOME left-handed settings returning the swapped button number.
122
-
123
- ### Design principle
124
-
125
- - Use the tkinter-native vocabulary per widget: listbox has "items", treeview has "rows", combobox has "values". The corresponding dynamic keys are `items_key`, `rows_key`, and `values_key`. This is more intuitive than forcing a single generic name across widgets.
126
-
127
- ### Deprecation note
128
-
129
- - `ListboxEvent` is soft-deprecated in v0.4.2. New code should use `EventSeq` directly. No removal timeline yet.
130
-
131
- ---
132
-
133
- ## Snapshot (v0.4.3) — filepicker + layout blocks
134
-
135
- - Added `@app.filepicker` (`filedialog` wrapper; `mode=` open/save/directory/open_multiple).
136
- - Added `Layout(spacing=...)` for a layout-wide SPACE token default (padx/pady inheritance).
137
- - Added `Layout.frame(...)` for nested layouts (`Layout` inside `Layout`).
138
- - Added `Layout.cluster(...)` wrapping flow: widgets keep content/`width=` sizes and wrap left-to-right.
139
- - Added `Layout.paired(left, right, ..., sync_yscroll=)` for side-by-side panes with optional text y-scroll sync.
140
- - Docs/examples/tests for the above (`filepicker_demo`, `nested_frame_demo`, `cluster_demo`, `paired_demo`).
141
-
142
- ---
143
-
144
- ## Snapshot (v0.4.4) — text wrap/h-scroll, public runtime API
145
-
146
- ### Text widget ergonomics (continued)
147
-
148
- - Added `wrap=` option to `@app.text` for logical-line display control:
149
- - `Wrap.WORD` (default) — wrap at word boundaries.
150
- - `Wrap.NONE` — keep each logical line on one row (requires horizontal scrolling).
151
- - `Wrap.CHAR` — wrap at any character boundary.
152
- - Added a new `nextpytk.types.Wrap` typed-constant class and `WrapLike` union.
153
- - Added `h_scroll=` option to `@app.text`: when True, a horizontal scrollbar is added (and wired to `xscrollcommand`) so long lines are reachable in `Wrap.NONE` mode. The horizontal scrollbar is tracked in `app._text_hscrollbars`.
154
-
155
- ### Decorator type-definition sync
156
-
157
- - `TreeviewOptions.double_click` is now typed and honored by `@app.treeview`.
158
- - Defaults to `True`: the `activate` handler fires on double-click (previous behavior).
159
- - When `False`, `activate` fires on a single `ButtonRelease-1` instead.
160
- - `_on_treeview_activate` accepts an optional `event` argument so the single-click binding can reuse it.
161
-
162
- ### Public runtime API
163
-
164
- - Added `app.layout_frame(name)` — returns the layout frame that owns a widget (the section frame), so applications can place/re-parent widgets with grid/pack without reaching into the private `_widget_masters`.
165
- - This is the public replacement for the internal `_widget_masters` access that previously forced applications to depend on private state.
166
-
167
- ### Per-widget design-token overrides
168
-
169
- - All widget decorators now accept `widget_kwargs: dict[str, Any]` (on `CommonWidgetOptions`).
170
- - Keys are widget-native tk/ttk options (`padx`, `pady`, `bg`, `fg`, `font`, …); values are the native values.
171
- - Overrides are applied after construction by `_apply_widget_overrides()`. An invalid/unknown key raises no error: a `TclError` is swallowed so one bad key does not abort the build.
172
- - This removes the need to monkey-patch the shared `tokens` module to tweak a single widget's appearance.
173
-
174
- ### Decorator argument validation
175
-
176
- - Added `_validate_choice()` / `_validate_positive_int()` helpers used by the decorators to reject invalid enum-like and non-positive-int options at **registration time** (before any Tk widget is built).
177
- - Validated options:
178
- - `text`: `state` (`normal`/`disabled`/`active`), `wrap` (`word`/`none`/`char`)
179
- - `scale`, `progressbar`, `paned`: `orient` (`horizontal`/`vertical`)
180
- - `listbox`, `treeview`: `selectmode` (`single`/`browse`/`multiple`/`extended`)
181
- - `filepicker`: `mode` (`open`/`open_multiple`/`save`/`directory`)
182
- - `progressbar`: `mode` (`determinate`/`indeterminate`)
183
- - A mistyped value (e.g. `wrap="wrap"`) now raises a clear `ValueError` naming the option, widget, and allowed values — instead of a cryptic `_tkinter.TclError` at runtime.
184
-
185
- ### Paired line-number gutters
186
-
187
- - Added `line_numbers=True` to `Layout.paired(...)`.
188
- - Each side gains a read-only line-number gutter (`disabled` tk.Text, `takefocus=0`, arrow cursor).
189
- - **Reliable sync via a single shared vertical scrollbar**: both panes and both gutters drive one shared scrollbar. Each widget's `yscrollcommand` is chained so scrolling any widget moves the other three, and the shared scrollbar's `command` drives all four. This avoids the `yview_moveto` drift that plagued per-widget gutter chaining.
190
- - Gutters stay in sync when content changes via `app.text_set()` (a new `app.on_text_set()` hook) and when the editable right pane is typed into (`<KeyRelease>`).
191
- - Added `examples/paired_demo.py` demonstrating `line_numbers=True`.
192
- - The new `app.on_text_set(name, hook)` public helper lets layout features react to programmatic text replacement.
193
-
194
- ### Dynamic layout switching (swap targets)
195
-
196
- - Added `Layout.target(name)` to reserve a swap region whose contents change at runtime (mirrors HTMX `hx-target`).
197
- - Added `@app.swap(name, variants=..., default=...)` to declare the layouts that can fill a target (mirrors HTMX `hx-swap`), and `app.swap_view(name, variant)` to switch imperatively.
198
- - Variants are mounted up-front and shown/hidden via `pack`/`pack_forget`, so widget state (treeview selection, scroll position) survives switching. Surrounding `Layout.section` blocks (toolbar, status) stay fixed.
199
- - Added `examples/swap_demo.py` (folder view <-> paired diff view).
200
-
201
- ### Tests
202
-
203
- - Added `tests/test_widget_public_api.py` covering `layout_frame` and `widget_kwargs` (applied, ignored-invalid).
204
- - Added `tests/test_decorator_validation.py` covering invalid-option rejection for every validated widget.
205
- - Added wrap/h-scroll tests to `tests/test_text.py`.
206
- - Added `double_click` tests to `tests/test_state.py`.
207
- - Added paired line-number gutter tests to `tests/test_paired.py` (creation, population, shared-scrollbar sync).
208
- - Added swap tests to `tests/test_swap.py` (variant registration, build, switching, back-and-forth, pre-build intent).
209
- - Suite grew from 181 to 208 passing; pyright clean.
210
-
211
- ---
212
-
213
- ## Snapshot (v0.4.5) — Flutter-style layout vocabulary
214
-
215
- ### `cluster` → `wrap` rename
216
-
217
- - `Layout.cluster()` renamed to `Layout.wrap()`, matching Flutter's `Wrap`
218
- widget. `cluster()` is kept as a deprecated alias (emits a
219
- `DeprecationWarning`) and will be removed in v0.5.0.
220
- - `LayoutBuilder.cluster()` → `LayoutBuilder.wrap()` (same alias story).
221
- - `examples/cluster_demo.py` → `examples/wrap_demo.py`; `tests/test_cluster.py`
222
- → `tests/test_wrap.py`.
223
- - `gap` keyword deprecated in favor of explicit `gapx`/`gapy` (SPACE tokens).
224
-
225
- ### `Flex` — absorb leftover space (Flutter `Expanded` analog)
226
-
227
- - Added `nextpytk.types.Flex(name, flex=...)`; a `wrap` child wrapped in `Flex`
228
- absorbs a share of its row's leftover horizontal space proportional to its
229
- flex factor, instead of keeping natural content width.
230
- - `widget_names()` and `mount_frames_into` resolve `Flex`-wrapped names.
231
-
232
- ### `FlowDelegate` / `Layout.flow` — custom flow (Flutter `Flow` analog)
233
-
234
- - Added `nextpytk.layout.Constraints` (width/height/min/max) and
235
- `nextpytk.layout.FlowDelegate` (abstract `compute_positions` /
236
- `compute_height`).
237
- - `Layout.flow(*widgets, delegate=...)` and `LayoutBuilder.flow(...)` position
238
- children via the delegate using `place`; recomputed on `<Configure>` resize.
239
- - `_place_flow` / `_Flow` block dispatched in `mount_frames_into` /
240
- `pack_children_for`.
241
- - `examples/wrap_demo.py` demonstrates `wrap`, `wrap`+`Flex`, and a
242
- `GridDelegate` flow.
243
-
244
- ### Theme / debug
245
-
246
- - Button styles use `width=0` (cancel clam's `width=-11` min ~182px) so button
247
- width tracks label text.
248
- - `_check_radio_padding` unified to (16,12) to match buttons;
249
- `indicatormargin=(0,0,SPACE[2],0)` adds 8px between indicator and label
250
- (clam ignores `indicatorpadding`).
251
- - `debug_layout()` detects geometry-manager conflicts;
252
- `check_layout_conflicts()` warns on each.
253
-
254
- ---
255
-
256
- ## Snapshot (v0.4.6) — section placement fix + layout ergonomics
257
-
258
- ### Bug fix: `section(..., side=...)` now controls the section frame placement
259
-
260
- - `_pack_section_frame` hardcoded `side="top"`, ignoring the requested
261
- placement side. A `side="bottom"` section declared after an `expand=True`
262
- block (e.g. a status bar under a growing body) was pushed off-view and
263
- collapsed to 1x1.
264
- - Fixed by honoring `block.side` on the section frame pack call.
265
-
266
- ### Child packing decoupled from frame placement
267
-
268
- - Added `_Row.child_side` to separate the section frame's placement side
269
- (`side`) from the side used to pack children *inside* the frame. A
270
- multi-widget section still lays its children out left-to-right (`"left"`)
271
- regardless of where the frame itself is placed (`side="top"`/`"bottom"`/
272
- `"left"`/`"right"`); a single-widget section packs its child with the
273
- frame's own placement side.
274
-
275
- ### Tests / example
276
-
277
- - Added `tests/test_layout_side.py` (regression coverage: frame side
278
- honored, default `top` preserved, multi-widget child `"left"` + frame
279
- `"top"`/`"bottom"`).
280
- - Added `examples/bottom_bar_demo.py` demonstrating a `side="bottom"` bar
281
- pinned below an expandable body.
282
-
283
- ### Known limitation: `side="bottom"` + focusable children
284
-
285
- `side="bottom"` controls only the pack **parcel** (visual placement). Tk's
286
- `tk_focusNext` / `tk_focusPrev` traverse the widget tree in *insertion order*
287
- (`winfo children`), which is independent of the pack parcel. Because a
288
- `side="bottom"` section must be declared early in the tree (so an
289
- `expand=True` sibling doesn't push it off-view), any focusable child in it
290
- (Entry, Button, …) lands **early** in the Tab order even though it is
291
- visually at the bottom.
292
-
293
- This is fine for non-focusable chrome (labels, a `status_bar`), but **not**
294
- for bottom-docked interactive widgets: visual order (top → bottom controls)
295
- and focus order (bottom controls first) diverge. The fix is to wire the
296
- visual tab order explicitly (the same `_wire_tab_order` used by `wrap` /
297
- `flow`, which intercepts `<Key-Tab>` / `<Shift-Key-Tab>` and moves focus in
298
- visual row-major order). Planned to generalize for `side="bottom"` sections.
299
-
300
- ---
301
-
302
- ## Snapshot (v0.4.7) — paired gutter logical-line fix
303
-
304
- ### Bug fix: line-number gutters count logical lines, not display lines
305
-
306
- - `Layout.paired(..., line_numbers=True)` gutters numbered **physical**
307
- (wrapped) rows: `_reconcile_gutter` / `_populate_gutters` used
308
- `text.index("end-1c")`, which counts display rows. With `wrap="word"` /
309
- `"char"`, a long line that wrapped inflated the gutter count beyond the
310
- real line count.
311
- - Added `_logical_line_count(text)`, which counts newline characters (a
312
- wrapped row adds no newline), adjusted for whether the buffer ends in a
313
- newline. The gutter now mirrors true logical lines regardless of wrapping.
314
-
315
- ### Bug fix: re-entry recursion in gutter sync
316
-
317
- - Rewriting a gutter calls `update_idletasks()`, which fires the shared
318
- scrollbar's `yscrollcommand` and re-enters `_reconcile_gutter` /
319
- `_populate_gutters` through `_chain_yview` — an infinite recursion that
320
- surfaced once the gutter logic changed.
321
- - Added a per-gutter `_syncing_gutter` re-entry guard to both helpers.
322
-
323
- ### Bug fix: `on_text_set` hook leak across re-runs
324
-
325
- - `app.on_text_set()` hooks were never cleared by `clear_runtime()`, so
326
- hooks (e.g. gutter line-number sync) accumulated across re-runs — such as
327
- swap variants rebuilt at runtime — causing the gutter sync to run multiple
328
- times per `text_set`.
329
- - `clear_runtime()` now also clears `_text_set_hooks`.
330
-
331
- ### Tests
332
-
333
- - Added `test_paired_gutters_count_logical_lines_when_wrapped` (a long,
334
- wrapping line must not inflate the gutter line numbers).
335
- - Added `test_paired_gutter_sync_does_not_recursively_loop` (multiple
336
- `text_set` calls do not recurse infinitely through the shared scrollbar).
337
- - Added `test_clear_runtime_clears_text_set_hooks` (hooks are dropped on
338
- `clear_runtime()`).
339
- - `tests/test_paired.py` grew; full suite green.
340
-
341
- ### Docs: `line_numbers=True` takes precedence over `sync_yscroll`
342
-
343
- - Documented that enabling `line_numbers` installs a shared scrollbar that
344
- always keeps both panes (and both gutters) in lock-step, so
345
- `sync_yscroll=False` is ignored when `line_numbers=True`. Callers needing
346
- independent pane scrolling must leave `line_numbers` off. Clarified in the
347
- `Layout.paired` docstring and README (EN / JA).
348
-
349
- ---
350
-
351
- ## Near term (post-0.4.7)
352
-
353
- ### A11y
354
-
355
- - [ ] `@app.status` live-region behavior (ARIA `aria-live` equivalent / WCAG 4.1.3)
356
- - [ ] Tk 9.1 `tk accessible set_acc_*` real-device verification (9.1b0 + NVDA)
357
- - [ ] Role vocabulary mapping to Tk side
358
- - [ ] Automatic `emit_selection_change` (leverage `apply_state` knowing which widgets changed)
359
-
360
- ### Type hints
361
-
362
- - [ ] Further TypedDict / Protocol coverage for decorator arguments
363
- - [ ] Fluent API equivalent to `_GridBuilder.columnconfigure`
364
-
365
- ### Widget expansion
366
-
367
- - [x] Per-widget `bind` for entry and listbox (`events=` on `@app.entry` and `@app.listbox`)
368
- - [x] Per-widget `bind` for other widgets via `widget_kwargs` / widget-level options
369
- - [ ] Per-widget `bind` for other widgets (`text`, `combobox`, `treeview`, `button`, etc.)
370
- - [x] listbox callback value design: keep selection index (unify with `treeview`)
371
- - [x] `@app.filepicker` — `filedialog` wrapper (schema/tool naming still open)
372
- - [x] Text `wrap=` / `h_scroll=` options (v0.4.4)
373
-
374
- ### Layout DSL
375
-
376
- - [x] Deprecate index-order plural methods `col_weights`/`row_weights`/`col_minsizes`/`row_minsizes` in 0.4.1 (remove in 0.5.0).
377
- - [ ] CSS-grid-inspired layout API for 0.5.0: explicit `grid-template-columns` / `grid-template-rows` strings (e.g. `"1fr 2fr"`, `"auto 1fr"`), area-based placement, and gap tokens. Reduce misuse by making rows/columns and widget placement visually aligned in one declaration.
378
- - [x] Nested frames (`Layout` inside `Layout`) via `Layout.frame(...)`
379
- - [x] Global default for `padx`/`pady` via `Layout(spacing=...)`
380
- - [x] `Layout.cluster(...)` wrapping flow; `Layout.paired(..., sync_yscroll=)`
381
- - [x] `Layout.cluster(...)` → `Layout.wrap(...)` rename with deprecated alias (v0.4.5)
382
- - [x] `Flex` (Flutter `Expanded` analog) for `wrap` (v0.4.5)
383
- - [x] `FlowDelegate` / `Layout.flow` custom flow (Flutter `Flow` analog) (v0.4.5)
384
- - [x] `Layout.paired(..., line_numbers=True)` read-only line-number gutters (v0.4.4)
385
- - [x] `Layout.target(...)` + `@app.swap(...)` dynamic region switching (v0.4.4)
386
-
387
- ### Agent / LLM integration
388
-
389
- - [ ] Expose `schema()` as Function Calling definition (bind `sequence`, treeview column defs, etc.)
390
- - [ ] `@agent_tool` integration (GUI operations as agent vocabulary)
391
- - [ ] Surface `@app.filepicker` in `schema()` / tool vocabulary
392
-
393
- ---
394
-
395
- ### v0.5.0 themes / chrome decoupling
396
-
397
- - Remove deprecated `theme=True/False` support; `theme` becomes `str` only.
398
- - Remove deprecated `EventSeq.DOUBLE_CLICK`; use `EventSeq.PRIMARY_DOUBLE_CLICK` or `DOUBLE_BUTTON_1`.
399
- - Decouple `Layout().header()` / `.status()` chrome from Kizashi-specific tokens so they render safely with any ttk theme (`"none"`, `"clam"`, etc.).
400
- - Extract Kizashi as a standalone ttk theme package so it can be proposed upstream or reused independently of nextpytk.
401
-
402
- ### Longer term
403
-
404
- ### ttk Style layer
405
-
406
- - [x] Theme switching (`ttk.Style().theme_use(...)`)
407
- - [ ] `Layout.style("my_button", background=..., font=...)` style definitions
408
-
409
- ### Declarative components
410
-
411
- - [ ] `@app.component` decorator (React-like reuse, replacing `Layout`)
412
- - [ ] State type definitions and validation (no Pydantic)
413
-
414
- ### Testing
415
-
416
- - [x] Headless execution tests (`tests/` — withdrawn root, build/apply_state/callback drive)
417
- - [ ] WidgetSpec unit tests
418
-
419
- ---
420
-
421
- ## Earlier snapshots
422
-
423
- ### v0.3.0
424
-
425
- Builder registry, headless tests, a11y wiring (`_apply_a11y`), error policy, bugfixes.
426
- `run_async` / `spawn` / `@app.job`, public runtime API (`build_widgets`, `apply_state`, `register_widget_builder`, …).
File without changes
File without changes
File without changes