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
12 changes: 9 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@
</p>

ColorCraft creates a palette from a source image or from colors that you enter
manually. It detects geometric hue relationships, evaluates contrast, proposes
optional colors, and generates browser-side exports.
manually. It reviews geometric hue relationships and defined color-pair
contrast, proposes optional colors, and generates browser-side exports.

![ColorCraft Review workspace](./docs/assets/screenshots/review-dark.png)

Expand All @@ -28,12 +28,18 @@ optional colors, and generates browser-side exports.
6. Use Export to generate **CSS custom properties**, **JSON**, **Tailwind theme colors**, or an **SVG swatch sheet**.
7. Select **Copy** or **Download**.

Relationship fit measures geometric agreement with documented hue structures, not aesthetic quality. Contrast checks measure one accessibility requirement; they do not prove complete WCAG conformance.
Relationship fit measures geometric agreement with documented hue structures,
not aesthetic quality. Text, non-text, and focus-indicator checks use the
threshold for the identified role pair. They do not prove complete WCAG
conformance.

The API accepts a source image up to 10 MB and 40 million decoded pixels. Extraction returns 3–10 requested colors, or fewer for a limited processing sample. **Save palette** stores a versioned record in IndexedDB. The Library can open, search, rename, duplicate, and delete saved palettes.

ColorCraft has no accounts or cloud synchronization. Source-image bytes and unsaved changes remain session-only. Browser history stores the active view and Review tab, not palette data. Export generation runs in the browser.

The shell reports **Loopback only** or **LAN enabled** from runtime metadata.
SVG exports select black or white swatch labels by measured contrast.

![ColorCraft Palette Library](./docs/assets/screenshots/library-dark.png)

## Quick start
Expand Down
2 changes: 1 addition & 1 deletion backend/accessibility.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ def wcag_rating(ratio):
Get WCAG rating for a contrast ratio.

Returns:
Dictionary with AA and AAA compliance for normal and large text
Dictionary with AA and AAA threshold results for normal and large text
"""
return {
"ratio": round(ratio, 2),
Expand Down
148 changes: 63 additions & 85 deletions backend/color_suggestions.py

Large diffs are not rendered by default.

25 changes: 22 additions & 3 deletions backend/color_theory.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
TETRADIC_TOLERANCE = 10.0
SPLIT_COMPLEMENTARY_TOLERANCE = 12.0
MONOCHROMATIC_TOLERANCE = 10.0
TEMPERATURE_DOMINANCE_THRESHOLD = 0.7


def normalize_hue(hue: float) -> float:
Expand Down Expand Up @@ -273,6 +274,7 @@ def detect_monochromatic(

def analyze_warm_cool_balance(colors: list[dict[str, object]]) -> dict[str, object]:
warm_count = 0
transitional_count = 0
cool_count = 0
for color in colors:
hsl = color["hsl"]
Expand All @@ -281,26 +283,43 @@ def analyze_warm_cool_balance(colors: list[dict[str, object]]) -> dict[str, obje
hue = normalize_hue(float(hsl["h"]))
if hue <= 60 or hue >= 300:
warm_count += 1
elif 120 <= hue <= 300:
elif hue < 120:
transitional_count += 1
else:
cool_count += 1

categorized = warm_count + cool_count
categorized = warm_count + transitional_count + cool_count
if categorized == 0:
return {
"balance": "neutral",
"warm_count": 0,
"transitional_count": 0,
"cool_count": 0,
"warm_ratio": 0.0,
"transitional_ratio": 0.0,
"cool_ratio": 0.0,
}
warm_ratio = warm_count / categorized
transitional_ratio = transitional_count / categorized
cool_ratio = cool_count / categorized
balance = "warm" if warm_ratio > 0.7 else "cool" if cool_ratio > 0.7 else "balanced"
ratios = {
"warm": warm_ratio,
"transitional": transitional_ratio,
"cool": cool_ratio,
}
dominant_category, dominant_ratio = max(ratios.items(), key=lambda item: item[1])
balance = (
dominant_category
if dominant_ratio > TEMPERATURE_DOMINANCE_THRESHOLD
else "mixed"
)
return {
"balance": balance,
"warm_count": warm_count,
"transitional_count": transitional_count,
"cool_count": cool_count,
"warm_ratio": round(warm_ratio, 2),
"transitional_ratio": round(transitional_ratio, 2),
"cool_ratio": round(cool_ratio, 2),
}

Expand Down
19 changes: 19 additions & 0 deletions backend/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,25 @@ def client_api_url(self) -> str:
host = self.api_host if is_loopback_host(self.api_host) else "127.0.0.1"
return origin_for(host, self.api_port)

@property
def network_mode(self) -> str:
"""Return the exposure mode from the resolved hosts and browser origins."""
configured_hosts = [self.web_host, self.api_host]
configured_hosts.extend(
parsed.hostname
for origin in self.allowed_origins
if (parsed := urlparse(origin)).hostname
)
if self.vite_api_url:
parsed_api_url = urlparse(self.vite_api_url)
if parsed_api_url.hostname:
configured_hosts.append(parsed_api_url.hostname)
return (
"loopback"
if all(is_loopback_host(host) for host in configured_hosts)
else "lan"
)

@classmethod
def from_env(
cls, environment: Mapping[str, str] | None = None
Expand Down
1 change: 1 addition & 0 deletions backend/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,7 @@ async def metadata() -> ApplicationMetadata:
api_url=runtime.client_api_url,
health_url=f"{runtime.client_api_url}/health",
readiness_url=f"{runtime.client_api_url}/ready",
network_mode=runtime.network_mode,
capabilities=CAPABILITIES,
)

Expand Down
7 changes: 5 additions & 2 deletions backend/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ class ApplicationMetadata(ContractModel):
api_url: str
health_url: str
readiness_url: str
network_mode: Literal["loopback", "lan"]
capabilities: list[str]


Expand Down Expand Up @@ -162,10 +163,12 @@ class HarmonyResults(ContractModel):


class TemperatureResults(ContractModel):
balance: Literal["warm", "cool", "balanced", "neutral"]
balance: Literal["warm", "transitional", "cool", "mixed", "neutral"]
warm_count: int = Field(ge=0)
transitional_count: int = Field(ge=0)
cool_count: int = Field(ge=0)
warm_ratio: float = Field(ge=0, le=1)
transitional_ratio: float = Field(ge=0, le=1)
cool_ratio: float = Field(ge=0, le=1)


Expand Down Expand Up @@ -238,7 +241,7 @@ class HarmonySuggestion(ContractModel):
angle: str
description: str
use_cases: list[str]
mood: str
common_associations: str
examples: str
suggestions: list[SuggestionColor]

Expand Down
23 changes: 20 additions & 3 deletions docs/api-contracts.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,15 @@ Each detected relationship contains:
Relationship fit describes measured geometry. It does not describe aesthetic
quality.

`temperatureBalance` contains:

- `balance`: `warm`, `transitional`, `cool`, `mixed`, or `neutral`
- `warmCount`, `transitionalCount`, and `coolCount`
- `warmRatio`, `transitionalRatio`, and `coolRatio`

The ratios use the total categorized meaningful-hue count. A category is
dominant only when its ratio is greater than 0.70.

## Contrast analysis

`accessibility` contains:
Expand All @@ -116,9 +125,13 @@ Each item in `suggestions` contains:
- `baseColor`
- `harmonies`

Each harmony item contains `type`, `angle`, `description`, `useCases`, `mood`,
`examples`, and `suggestions`. Each suggested color contains `name`,
`description`, HEX, RGB, and HSL values.
Each harmony item contains `type`, `angle`, `description`, `useCases`,
`commonAssociations`, `examples`, and `suggestions`. Each suggested color
contains `name`, `description`, HEX, RGB, and HSL values.

`commonAssociations` is conventional guidance. It is not a measured result.
This field replaces the removed `mood` field and is a breaking response-contract
change.

## Metadata and readiness

Expand All @@ -133,8 +146,12 @@ Each harmony item contains `type`, `angle`, `description`, `useCases`, `mood`,
- `apiUrl`
- `healthUrl`
- `readinessUrl`
- `networkMode`: `loopback` or `lan`
- `capabilities`

`networkMode` comes from resolved hosts and browser-visible origins. It does
not come only from the LAN opt-in environment variable.

`GET /ready` returns HTTP 200 with `status: "ready"` after startup. Before
startup completes, it can return HTTP 503 with `status: "not_ready"`.

Expand Down
19 changes: 14 additions & 5 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,13 +85,17 @@ The frontend sends 2–10 normalized palette colors to
6. Returns a camelCase `analysis` contract.

The frontend combines the API analysis with current role assignments. Contrast
shows role-specific checks and an advanced all-pairs contrast matrix.
uses an explicit `text`, `nonText`, or `focus` discriminator for each role
check. Text checks use AA and AAA text thresholds. Non-text and focus color
checks use 3:1 without text badges. The advanced matrix remains a text-contrast
exploration table.

## Suggestions flow

The frontend sends 1–10 colors to `POST /api/suggest-colors`. The API generates
suggestion approaches for each base color. Suggestions do not change the
palette automatically. The user must select **Add**.
geometric suggestion approaches for each base color. `commonAssociations` and
`useCases` contain qualified conventional guidance. Suggestions do not change
the palette automatically. The user must select **Add**.

The frontend fingerprints the current palette. A palette color change
invalidates the displayed suggestion results.
Expand All @@ -106,8 +110,9 @@ The frontend generates every export without an API request:
- SVG swatch sheet

CSS and Tailwind comments replace line breaks and the `*/` sequence in the
palette name. SVG output escapes `&`, `<`, `>`, `"`, and `'`. Download uses an
object URL and revokes the URL after the browser starts the download.
palette name. SVG output escapes `&`, `<`, `>`, `"`, and `'`. Each swatch label
uses black or white according to the higher measured contrast ratio. Download
uses an object URL and revokes the URL after the browser starts the download.

Export does not create or update a saved palette record.

Expand All @@ -130,6 +135,10 @@ The default services bind to `127.0.0.1`. CORS accepts the resolved web origin.
Wildcard origins are rejected. Non-loopback hosts and origins require
`COLORCRAFT_ALLOW_LAN_ACCESS=true`.

Runtime metadata reports `networkMode` from resolved hosts and browser origins.
The frontend uses this field for the shell status and does not infer exposure
from a display URL.

ColorCraft does not provide authentication. Trusted LAN access expands the
network boundary. Do not expose the development API directly to an untrusted
network.
Expand Down
Binary file added docs/assets/screenshots/contrast-dark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/assets/screenshots/review-dark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/assets/screenshots/suggestions-dark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
9 changes: 7 additions & 2 deletions docs/brand-system.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ local utility rather than a dashboard and does not imply accounts or cloud sync.
- Prefer concrete verbs such as extract, inspect, compare, and add.
- Avoid generic AI language and inflated claims.
- Keep helper text short and place detail beside the result it qualifies.
- Use **review** or **measure** for defined properties. Do not say that
ColorCraft validates aesthetic quality or complete accessibility.
- Use **Loopback only** and **LAN enabled** for resolved network status. Do not
substitute *Local only*, *Secure*, *Private*, or *Offline*.
- Present suggestion associations as conventional guidance, not measured fact.

This document governs product voice, visual identity, and interface tone.
[ColorCraft Technical English](./writing-style.md) governs procedural and
Expand Down Expand Up @@ -85,8 +90,8 @@ menus use elevated shadow, and modal dialogs use dialog shadow.

- Desktop uses a compact 256px sidebar with product identity, New palette,
workflow navigation, and theme selection.
- Workspace headers describe the current palette source and always state that
work is local only.
- Workspace headers describe the current palette source and show the
metadata-derived network state as `Loopback only` or `LAN enabled`.
- Create is always available. Review and Export reflect real palette
prerequisites and explain disabled states.
- At 1024px and below, the sidebar becomes a mobile top bar and bottom
Expand Down
44 changes: 34 additions & 10 deletions docs/color-analysis.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,8 +132,17 @@ accessibility score, or design recommendation.
- Hue diversity is circular standard deviation in degrees, capped at 180.
- Average saturation is the arithmetic mean of HSL saturation.
- Lightness range is the maximum HSL lightness minus the minimum.
- Temperature ratios count meaningful hues in the current warm and cool
intervals. They do not evaluate aesthetic balance.
- Temperature evidence assigns every meaningful hue to one category:
- Warm: 300 through 360 degrees and 0 through 60 degrees
- Transitional: greater than 60 and less than 120 degrees
- Cool: 120 through less than 300 degrees
- A hue at 300 degrees is warm so that the categories do not overlap.
- Low-saturation colors are excluded from temperature evidence.
- `warmRatio`, `transitionalRatio`, and `coolRatio` use all categorized
meaningful hues as the denominator.
- A category is dominant when its ratio is greater than 70%. The result is
`mixed` when no category exceeds 70% and `neutral` when no meaningful hue
evidence exists.

## Contrast calculation

Expand All @@ -150,7 +159,7 @@ Contrast ratio is:
(lighter luminance + 0.05) / (darker luminance + 0.05)
```

The API reports:
The API all-pairs result reports text thresholds:

| Result | Minimum ratio |
| --- | ---: |
Expand All @@ -159,26 +168,40 @@ The API reports:
| WCAG AAA normal text | 7:1 |
| WCAG AAA large text | 4.5:1 |

The API evaluates every palette color pair. Review also evaluates assigned
color roles in meaningful interface combinations.
The API evaluates every palette color pair as text-contrast exploration data.
Review applies the ratio to typed role checks:

- Text checks show the AA and AAA normal-text and large-text thresholds.
- Non-text component checks use a 3:1 threshold and do not show text badges.
- Focus-indicator color checks use a 3:1 threshold for each evaluated adjacent
color pair.

A non-text result evaluates color contrast only. It does not evaluate component
size, shape, state, or other accessibility requirements. A focus-indicator
result does not evaluate size, area, thickness, visibility, or the
focused-versus-unfocused appearance.

A passing ratio applies only to the evaluated contrast pair and text category.
It does not prove complete interface accessibility or WCAG conformance.

## Suggestions

Suggestions are optional calculations from a selected base color. A suggestion
does not confirm that a detected relationship exists in the current palette.
The frontend invalidates suggestions when any palette color changes.
Suggestions are optional geometric transformations from a selected base color.
Descriptions lead with hue, saturation, or lightness changes. `useCases` and
`commonAssociations` contain conventional guidance, not measured suitability.
A suggestion does not confirm that a detected relationship exists in the
current palette. The frontend invalidates suggestions when any palette color
changes.

## Export accuracy and escaping

JSON export uses `schemaVersion: 1`. JSON includes ordered colors, normalized
color values, optional population, color-role metadata, and role assignments.

CSS and Tailwind output sanitize palette names used in comments. SVG output
escapes XML-sensitive characters. Export generation does not alter the palette
and does not save it to IndexedDB.
escapes XML-sensitive characters. Each SVG swatch compares measured contrast
against black and white and uses the label color with the higher ratio. Export
generation does not alter the palette and does not save it to IndexedDB.

## Interpretation limits

Expand All @@ -190,3 +213,4 @@ and does not save it to IndexedDB.
- Contrast is independent of hue geometry.
- Contrast evaluation covers one accessibility requirement.
- Suggestions are recommendations, not measurements.
- Temperature categories describe hue intervals, not aesthetic balance.
8 changes: 6 additions & 2 deletions docs/dashboard-manifest.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ Static addresses are defaults only. Environment variables can change ports or ho

1. Discover the application from `app-manifest.json`.
2. Query the manifest's metadata path on the candidate API address.
3. Treat `/metadata` as authoritative for runtime-resolved URLs.
3. Treat `/metadata` as authoritative for runtime-resolved URLs and
`networkMode`.
4. Use `/ready` for dependency readiness and `/health` for liveness.

Current capability slugs are:
Expand All @@ -20,4 +21,7 @@ Current capability slugs are:
- `palette-export`
- `local-palette-library`

The manifest contract and public copy are tested for equality. Runtime tests verify that metadata respects configuration overrides.
The manifest contract and public copy are tested for equality. Runtime tests
verify that metadata respects configuration overrides. The static manifest
does not guess the network exposure mode. Runtime metadata reports `loopback`
or `lan` from the resolved configuration.
5 changes: 5 additions & 0 deletions docs/persistence-and-privacy.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,5 +60,10 @@ source-image data traverses the configured network path. ColorCraft does not
provide authentication or transport encryption. Do not expose it directly to
an untrusted network.

Runtime metadata reports `networkMode: "loopback"` when every resolved host and
browser origin is loopback. It reports `networkMode: "lan"` when any resolved
host or origin is non-loopback. The shell presents these values as **Loopback
only** and **LAN enabled**.

Local does not automatically mean private, secure, offline, anonymous, or
persistent.
Loading
Loading