Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 22 additions & 23 deletions docs/UIUX_BRANDING_HANDOFF.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -80,16 +80,17 @@ Flags:

### Questionary prompt flows

Interactive mode uses `questionary` with consistent markers: `qmark="?"`, `pointer=">"`.
Interactive mode uses `questionary` with `CPA_PROMPT_STYLE` (high-contrast blue/green),
`qmark="?"`, and `pointer="❯"`.

#### 1. Template selection (autocomplete)
#### 1. Template selection (select + search filter)

When no `--template` is provided and interactive mode is on:

- Prompt: `Pick a template (type to search)`
- Control: `questionary.autocomplete` with `match_middle=True`
- Choices: built from `build_template_choices()` in `catalog.py`
- Each choice shows a category badge, template name, slug, optional labels, and description
- Prompt: `Pick a template`
- Control: `questionary.select` with `use_search_filter=True` (↑↓ browse, type to filter)
- Choices: `Choice(title, value)` from `build_template_choices()` in `catalog.py`
- Each choice shows a colored category badge, bold name, slug, optional labels, and description
- Final choice: `Use my own template URL` -> `questionary.text` for a custom URL

#### 2. Extension selection (checkbox, two-step)
Expand All@@ -110,17 +111,15 @@ From `cpa.config.json` or catalog `customOptions`:

### Category badges

Interactive template choices use a plain fixed-width badge from
`short_category_label()` (strips "Applications", "Application", "Boilerplate";
abbreviates long names to initials). Titles stay plain text because
`questionary.autocomplete` wraps choices in HTML for match highlighting — ANSI
or other markup raises XML parse errors in prompt_toolkit.
Interactive template choices use a fixed-width badge from `short_category_label()`
with bright bold ANSI colors (`prompt_style.color_category`) so they stay readable
on dark terminals. Titles may include ANSI because the picker is
`questionary.select(..., use_search_filter=True)` — **not** autocomplete (which
HTML-parses choice text and breaks on ANSI).

`--list-templates` uses Rich tables for color, not ANSI in choice strings.
Respects `NO_COLOR`. `--list-templates` uses Rich tables for color.

Design implication: if terminal category color returns, prefer Rich styling or
a prompt library that does not HTML-parse choice titles (CNA uses `prompts` +
picocolors).
UX: ↑↓ browse the full catalog, type to filter, Enter to pick (CNA-parity discovery).

### Rich semantic color usage

Expand DownExpand Up@@ -171,7 +170,7 @@ Completed in `create-python-app`:
- Package README at `packages/create-awesome-python-app/README.md` with minimal hero SVG reference.
- Placeholder hero SVG at `packages/create-awesome-python-app/assets/hero.svg` (dark background, teal text).
- Working brand notes in `docs/BRAND.md` (tagline and story).
- Full interactive CLI with CNA-parity flows (autocomplete templates, checkbox extensions, custom options).
- Full interactive CLI with CNA-parity flows (select+filter templates, checkbox extensions, custom options).
- Rich stderr output and semantic coloring across scaffold and cache commands.
- Catalog integration with `cpa-templates` default URL and `CPA_CATALOG_URL` override.

Expand All@@ -191,7 +190,7 @@ The CLI experience is functional and CNA-aligned for catalog flows, but the broa
Current CLI aesthetic:

- Rich semantic colors (red/yellow/green/cyan/dim).
- Plain fixed-width category badges in autocomplete titles (HTML-safe for questionary).
- Bright category badges + high-contrast `CPA_PROMPT_STYLE` on select/checkbox prompts.
- Minimal hero SVG (slate + teal).
- No startup banner or branded prompt chrome beyond questionary defaults.

Expand All@@ -214,7 +213,7 @@ The user wants a complete review and improvement cycle for:
- Visual consistency between GitHub, PyPI, docs, CLI, and templates.
- Engagement and adoption.
- A more cozy, attractive, polished, memorable brand.
- CLI prompt flow polish (autocomplete, checkbox, error tone, banner).
- CLI prompt flow polish (select+filter, checkbox, error tone, banner).

The user explicitly wants the new chat to review everything, not only CLI internals or README tweaks.

Expand DownExpand Up@@ -289,7 +288,7 @@ Before implementation, produce a complete audit answering:
- What is the current brand personality?
- What should the brand personality become?
- Does the CLI first run explain the product clearly in the first 5 seconds?
- Do autocomplete and checkbox flows feel premium and discoverable?
- Do select+filter and checkbox flows feel premium and discoverable?
- Should website/docs category badges use semantic colors (CLI titles stay plain for questionary)?
- Does the PyPI package README convert visitors into users?
- Does the root GitHub README convert visitors into contributors?
Expand DownExpand Up@@ -363,7 +362,7 @@ Visual identity:
- Define illustration/hero style.
- Define voice and tone (match CLI error copy guidelines).
- Define how "cozy" and "developer infrastructure" coexist.
- Define category badge colors for website/docs cards (CLI autocomplete titles stay plain text).
- Define category badge colors for website/docs cards (CLI already uses bright ANSI badges).

## Constraints And Standards

Expand DownExpand Up@@ -420,9 +419,9 @@ We need to do a full UI/UX and branding review of the Create Python App ecosyste

Please start with discovery and audit before implementing. Review the root create-python-app repo, the package README, cpa-templates, and docs/BRAND.md. The goal is to improve engagement, attraction, branding, cozy developer experience, visual consistency, and conversion across GitHub, PyPI, docs, CLI, and generated starter UIs.

The CLI already uses Rich on stderr and questionary autocomplete/checkbox flows
with plain-text category badges (HTML-safe for prompt_toolkit). Evaluate whether
those defaults should evolve into a cohesive brand system. Pay attention to error
The CLI already uses Rich on stderr and questionary select/checkbox flows with
`CPA_PROMPT_STYLE` plus bright ANSI category badges. Evaluate whether those
defaults should evolve into a cohesive brand system. Pay attention to error
message tone, interactive vs CI non-interactive behavior, and the minimal hero SVG.

Previous work established basic READMEs and BRAND.md notes, but now I want a broader review and a stronger cohesive brand direction. Do not assume the current teal-on-slate hero or terminal colors are final.
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -16,6 +16,7 @@
from rich.table import Table

from create_awesome_python_app import __version__
from create_awesome_python_app.prompt_style import bold, color_category, dim

console = Console(stderr=True)

Expand DownExpand Up@@ -238,14 +239,17 @@ def build_template_choices(data: dict[str, Any]) -> list[TemplateChoice]:
labels = template.get("labels", [])
label_suffix = ""
if isinstance(labels, list) and labels:
label_suffix = " · " + ", ".join(str(label) for label in labels[:3])
label_suffix = dim(" · " + ", ".join(str(label) for label in labels[:3]))
description = str(template.get("description", "")).strip()
description_suffix = f" — {description}" if description else ""
# Plain text only: questionary.autocomplete wraps choices in HTML for
# match highlighting, so ANSI / markup here raises XML parse errors.
# Keep slug + short description in the title so select(use_search_filter)
# can match them (filter scans Choice.title only).
description_suffix = dim(f" — {description}") if description else ""
name = str(template.get("name", slug))
# ANSI is OK here: questionary.select renders titles as terminal text.
# Do not pass these titles to autocomplete (HTML match highlighting).
title = (
f"{badge} "
f"{template.get('name', slug)} ({slug})"
f"{color_category(category_slug, badge)} "
f"{bold(name)} ({slug})"
f"{label_suffix}{description_suffix}"
)
choices.append(
Expand All@@ -258,7 +262,7 @@ def build_template_choices(data: dict[str, Any]) -> list[TemplateChoice]:

choices.append(
TemplateChoice(
title=" " * 12 + "Use my own template URL",
title=" " * 12 + dim("Use my own template URL"),
value=CUSTOM_TEMPLATE_SENTINEL,
search="custom own template url github file",
)
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -104,6 +104,8 @@ def _prompt_custom_options(
) -> dict[str, str]:
import questionary

from create_awesome_python_app.prompt_style import CPA_PROMPT_STYLE

source = resolve_source(template, cache_dir=cache_dir)
root = download_repository(
source,
Expand DownExpand Up@@ -138,9 +140,12 @@ def _prompt_custom_options(
answer = questionary.confirm(
message,
default=initial.lower() in {"1", "true", "yes", "on"},
style=CPA_PROMPT_STYLE,
).ask()
else:
answer = questionary.text(message, default=initial).ask()
answer = questionary.text(
message, default=initial, style=CPA_PROMPT_STYLE
).ask()
if answer is None:
raise typer.Exit(1)
answers[option.key] = _stringify_option_value(answer)
Expand DownExpand Up@@ -230,30 +235,38 @@ def scaffold(
if want_interactive and not template:
try:
import questionary
from questionary import Choice

from create_awesome_python_app.catalog import (
CUSTOM_TEMPLATE_SENTINEL,
build_template_choices,
get_catalog_data,
)
from create_awesome_python_app.prompt_style import CPA_PROMPT_STYLE

interactive_catalog = get_catalog_data()
template_choices = build_template_choices(interactive_catalog)
choice_by_title = {
choice.title: choice.value for choice in template_choices
}
selected_title = questionary.autocomplete(
"Pick a template (type to search)",
choices=list(choice_by_title),
match_middle=True,
# select + type-to-filter: browseable list (CNA-style discovery)
# instead of autocomplete-only. use_jk_keys must be False with search.
selected_template = questionary.select(
"Pick a template",
choices=[
Choice(title=choice.title, value=choice.value)
for choice in template_choices
],
qmark="?",
pointer="❯",
style=CPA_PROMPT_STYLE,
use_search_filter=True,
use_jk_keys=False,
instruction="(↑↓ browse · type to filter · Enter)",
).ask()
selected_template = choice_by_title.get(str(selected_title), selected_title)
if selected_template == CUSTOM_TEMPLATE_SENTINEL:
selected_template = questionary.text(
"Template URL",
default="file://.",
validate=lambda value: bool(value) or "Template URL is required",
style=CPA_PROMPT_STYLE,
).ask()
template = selected_template
if not template:
Expand DownExpand Up@@ -296,6 +309,7 @@ def scaffold(
get_catalog_data,
group_extension_choices,
)
from create_awesome_python_app.prompt_style import CPA_PROMPT_STYLE

interactive_catalog = interactive_catalog or get_catalog_data()
extension_choices = build_extension_choices(interactive_catalog, template)
Expand All@@ -316,7 +330,8 @@ def scaffold(
"Which kinds of extensions do you need?",
choices=category_choices,
qmark="?",
pointer=">",
pointer="❯",
style=CPA_PROMPT_STYLE,
).ask()
selected_addons: list[str] = []
for category_slug in selected_categories or []:
Expand All@@ -330,7 +345,8 @@ def scaffold(
for choice in choices
],
qmark="?",
pointer=">",
pointer="❯",
style=CPA_PROMPT_STYLE,
).ask()
selected_addons.extend(str(item) for item in picked or [])
addons = selected_addons
Expand Down
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
"""High-contrast questionary styles for CPA interactive prompts."""

from __future__ import annotations

import os

from questionary import Style

# Lighter brand blues/greens than docs hex so selected rows stay readable on
# dark terminals (slate backgrounds common in Arch/Ghostty/Alacritty).
CPA_PROMPT_STYLE = Style.from_dict(
{
"qmark": "fg:#60a5fa bold",
"question": "bold fg:#f8fafc",
"answer": "fg:#4ade80 bold",
"pointer": "fg:#60a5fa bold",
"highlighted": "fg:#0f172a bg:#60a5fa bold",
"selected": "fg:#4ade80 bold",
"separator": "fg:#94a3b8",
"instruction": "fg:#94a3b8",
"text": "fg:#e2e8f0",
"disabled": "fg:#64748b italic",
"checkbox": "fg:#60a5fa",
"checkbox-selected": "fg:#4ade80 bold",
}
)


def colors_enabled() -> bool:
return not os.environ.get("NO_COLOR")


def ansi(code: str, text: str) -> str:
"""Wrap *text* in an ANSI SGR sequence when colors are enabled."""
if not colors_enabled():
return text
return f"\033[{code}m{text}\033[0m"


# Bold bright ANSI — readable on dark terminals; select() renders these safely
# (unlike autocomplete, which HTML-parses choice text).
_CATEGORY_PALETTE = (
"1;93", # bright yellow
"1;92", # bright green
"1;96", # bright cyan
"1;95", # bright magenta
"1;94", # bright blue
)


def color_category(slug: str, label: str) -> str:
idx = sum(ord(char) for char in slug) % len(_CATEGORY_PALETTE)
return ansi(_CATEGORY_PALETTE[idx], label)


def bold(text: str) -> str:
return ansi("1", text)


def dim(text: str) -> str:
return ansi("2", text)
34 changes: 26 additions & 8 deletions packages/create-awesome-python-app/tests/test_catalog_resolve.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -102,17 +102,37 @@ def test_build_template_choices_are_searchable() -> None:
assert "FastAPI Starter" in first.title
assert "OpenAPI" in first.title
assert "uv" in first.title
assert "\033" not in first.title
assert "openapi" in first.search
assert "backend" in first.search
assert "uv" in first.search
assert choices[-1].value == CUSTOM_TEMPLATE_SENTINEL


def test_template_choice_titles_are_html_safe_for_questionary() -> None:
"""questionary.autocomplete formats choice text as HTML (match underline)."""
from prompt_toolkit.formatted_text import HTML
def test_template_choice_titles_include_bright_category_ansi(
monkeypatch,
) -> None:
"""select() can render ANSI; badges use bright bold codes for contrast."""
monkeypatch.delenv("NO_COLOR", raising=False)
catalog = {
"categories": [
{"slug": "backend-applications", "name": "Backend Applications"}
],
"templates": [
{
"slug": "fastapi-starter",
"name": "FastAPI Starter",
"url": "file:///templates/fastapi",
"category": "backend-applications",
}
],
}
title = build_template_choices(catalog)[0].title
assert "\033[" in title
assert "FastAPI Starter" in title


def test_template_choice_titles_respect_no_color(monkeypatch) -> None:
monkeypatch.setenv("NO_COLOR", "1")
catalog = {
"categories": [
{"slug": "backend-applications", "name": "Backend Applications"}
Expand All@@ -121,16 +141,14 @@ def test_template_choice_titles_are_html_safe_for_questionary() -> None:
{
"slug": "fastapi-starter",
"name": "FastAPI Starter",
"description": "Async API with OpenAPI docs",
"url": "file:///templates/fastapi",
"category": "backend-applications",
"labels": ["FastAPI"],
}
],
}
title = build_template_choices(catalog)[0].title
# Must not raise "not well-formed (invalid token)" from ANSI escapes.
HTML("{}<b><u>{}</u></b>{}").format(title[:3], title[3:6], title[6:])
assert "\033" not in title
assert "FastAPI Starter" in title


def test_build_extension_choices_filters_by_template_type() -> None:
Expand Down
Loading
Loading