0.5.0 --- VIS Widgets, Help Button & Popup Polish
==================================================

*Released --- current version*

Adds general-purpose widgets, a one-line Help-button hookup, standard
confirmation dialogs, and a flicker-free popup centring helper.  The
project-upgrade-tool work originally planned for 0.5 has been deferred
(see "Out of scope" below).

VIS Widgets
-----------

General-purpose widgets that Tkinter does not provide natively.  All
exported from :mod:`VIStk.Widgets`.

- ``Tooltip`` --- hover tooltip bound to any widget.  ``text`` may be a
  ``str`` or a zero-arg callable for state-dependent tooltips.  Cleans
  up its ``after`` callback on widget destroy::

      from VIStk.Widgets import Tooltip
      Tooltip(my_button, text="Save the current document")

- ``CollapsibleFrame`` --- frame whose body is hidden under a header
  button.  Pack children into ``cf.body``; ``cf.expanded_var`` is a
  ``BooleanVar`` callers can bind for shared state::

      cf = CollapsibleFrame(parent, text="Advanced", expanded=False)
      cf.pack(fill="x")
      ttk.Entry(cf.body).pack()

- ``AutocompleteEntry`` --- :class:`ttk.Entry` with a filtered dropdown
  ``Listbox``.  ``values`` is either an iterable or a callable
  ``(text) -> iterable``.  Keyboard: ``Up``/``Down`` move,
  ``Return`` accepts, ``Tab`` accepts the first match, ``Escape``
  closes.  ``match="prefix"`` (default) or ``"contains"``.

- ``DateEntry`` --- :class:`ttk.Entry` + calendar-picker button.  No
  third-party dependencies.  ``get()`` returns ``date | None``,
  ``set(date)`` sets programmatically, invalid manual input reverts to
  the last valid value on focus-out.

Confirmation Dialogs
--------------------

Drop-in helpers so screens stop reimplementing the
:mod:`tkinter.messagebox` dance::

    from VIStk.Widgets import confirm, confirm_discard

    if confirm(parent, title="Delete?", message="Really delete?"):
        ...

    choice = confirm_discard(parent, name="Work Order #12345")
    if choice == "cancel":
        return False                # veto on_quit
    if choice == "save":
        _save()
    return True

- ``confirm(...) -> bool`` --- two-button Yes/No.
- ``confirm_discard(...) -> "save" | "discard" | "cancel"`` --- three-
  button Save / Discard / Cancel.  Closing the window or pressing
  Escape both return ``"cancel"``.

Both dialogs are modal, transient over their parent, centred via
:meth:`WindowGeometry.center_on`, and never flash at the OS default
position before centring.

Help Button & Per-Screen ``docs`` URL
-------------------------------------

Wiring a top-level Help button is a one-liner from ``.VIS/Host.py``::

    from VIStk.Widgets import HostMenu
    from VIStk.Objects import open_active_screen_docs

    host_menu.add_project_command("Help", open_active_screen_docs)

``HostMenu.add_project_command(label, command)``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

New companion to ``set_project_items``.  Adds a clickable leaf entry
**directly** on the menubar (not a cascade).  Survives all tab changes;
removed only by ``clear_project_items``.

Per-screen ``docs`` field
~~~~~~~~~~~~~~~~~~~~~~~~~

Each entry under ``Screens.<name>`` in ``project.json`` may carry a
``docs`` URL::

    "Screens": {
      "Home":     {"...": "...", "docs": null},
      "Settings": {"...": "...", "docs": "https://example.com/docs/settings"}
    },
    "defaults": {
      "icon": "...",
      "docs": "https://example.com/docs"
    }

- ``Screen.docs: str | None`` --- per-screen URL.  ``None`` means
  "fall through to project default".
- ``Project.default_docs: str | None`` --- project-level fallback.
- ``Project.resolve_docs_url(screen_name=None)`` --- runs the fallback
  chain (screen ``docs`` -> ``default_docs`` -> ``None``).
- ``Project.active_screen_name`` --- property returning the active tab's
  ``base_name`` (resolved from the live tab ID introduced in 0.4.7).
- ``VIStk.Objects.open_active_screen_docs()`` --- looks up the active
  screen's URL via ``Project().resolve_docs_url()`` and hands it to
  :func:`webbrowser.open`.  Returns ``True`` on dispatch, ``False`` if
  no URL was configured.

URLs are passed verbatim to :func:`webbrowser.open`.  No path
normalisation --- authors write fully-qualified URLs (``https://``,
``file:///``, etc.).

``VIS docs`` CLI
~~~~~~~~~~~~~~~~

.. code-block:: text

   VIS docs set <screen_name> <url>      # set a per-screen URL
   VIS docs set --default <url>          # set the project default
   VIS docs clear <screen_name>          # clear a per-screen URL
   VIS docs clear --default              # clear the project default
   VIS docs list                         # show all configured URLs

The scaffolder (``VIS add screen``) writes ``"docs": null`` into new
entries so the field is discoverable.

Popup Flicker Fix --- ``WindowGeometry.center_on``
--------------------------------------------------

The canonical *update + getGeometry + setGeometry* pattern made every
centred popup flash at the OS default position before jumping to the
final centred coordinates.  ``center_on`` does the same math inside a
``withdraw()`` / ``deiconify()`` wrap and uses ``update_idletasks()``
(layout-only) instead of ``update()`` (which also processes the map
request)::

    popup = Toplevel(root)
    # ... build child widgets ...
    WindowGeometry(popup)
    popup.WindowGeometry.center_on(root)

Not intended for the root ``Tk()`` window --- ``withdraw()`` on the
main application window hides it entirely.  ``setGeometry`` itself is
unchanged.

Out of Scope (deferred)
-----------------------

- **Project Upgrade Tool** (``VIS upgrade``) --- the originally planned
  0.5 epic is deferred to 0.7 (which is already titled "Defaults &
  Navigation, update tools" --- a better fit).
- **Color palette feature** --- deferred; tracked separately for a
  later 0.5.x or 0.6.x.
- **``is_dirty`` auto-generated ``on_quit``** --- the
  ``confirm_discard`` helper covers the manual case; the auto-wrapper
  can land later without an API break.
