From 588d65488ba42d57cce50931eea14d2eb701d771 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A5rd=20Farstad?= Date: Tue, 15 Sep 2026 14:39:49 +0200 Subject: [PATCH] feat(skills): document the Colors component MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Colors component was in no skill at all. Worse, components.md actively pointed the other way: "color" was the headline example for when to use a Selection, so an agent modelling colour would reach for a dropdown of colour names and never learn the component exists. Adds Colors to the content model, and corrects the advice around it. content-model/references/components.md - new "I need to store a colour" scenario in the decision guide - a "Colors vs Selection" section, because that is the decision people get wrong: Colors holds the value you render, a Selection or topic holds the token you filter on, and most catalogues need both - Selection's example list no longer claims colour - rows in the localization-defaults and validations tables - a branch in the component selection flowchart - a note under Discoverability that a hex is a poor facet content-model/references/create-shape-api.md - `colors` added to the ComponentType enum - full config block: min/max, requireLabel, colorPresets, customColorSpaces mutation/references/core-api.md - updateComponent example, including the doubled `colors: { colors: [] }` key, which is easy to get wrong query/references/catalogue-api.md - reading ColorsContent, and the nullability of every notation data-creation/SKILL.md - added to the skip list: entries are structured objects, not strings The modelling point the whole thing turns on is that one entry carries several notations of the same colour at once — hex and rgb for screen, cmyk and pantone for print, ral for paint. Splitting those across Single Line components guarantees they drift apart the first time a colour is corrected. Verified against the live Core API (ColorsComponentConfig, ColorsComponentContentInput, GraphqlInputColorEntry, the ColorSpace enum) and the Catalogue API (ColorsContent, ColorEntry). Not documented, because it could not be verified: how a discoverable `colors` component is indexed in Discovery. The token this repo's access allows could not read shapes on any tenant using the component. The guidance given does not depend on the answer — filter on the name, not the value — but the indexed field shape is deliberately absent rather than guessed. Co-Authored-By: Claude Opus 5 (1M context) --- use-crystallize/skills/content-model/SKILL.md | 2 +- .../content-model/references/components.md | 97 +++++++++++++------ .../references/create-shape-api.md | 35 ++++++- use-crystallize/skills/data-creation/SKILL.md | 30 +++--- .../skills/mutation/references/core-api.md | 36 +++++++ .../skills/query/references/catalogue-api.md | 40 ++++++++ 6 files changed, 194 insertions(+), 46 deletions(-) diff --git a/use-crystallize/skills/content-model/SKILL.md b/use-crystallize/skills/content-model/SKILL.md index 7ba89b4..d89f189 100644 --- a/use-crystallize/skills/content-model/SKILL.md +++ b/use-crystallize/skills/content-model/SKILL.md @@ -1,6 +1,6 @@ --- name: content-model -description: Design content models in Crystallize using Shapes, Pieces, Components, Topic Maps, and Grids. Create product structures, define document types, build taxonomies, organize catalogue items, design relationships between items, implement classification bridges, configure item relations with shape restrictions, and architect scalable data models. Use when modeling content, creating shapes, defining components, building taxonomies, designing relationship patterns, implementing semantic bridges, configuring product variants, or structuring catalogue hierarchies. +description: Design content models in Crystallize using Shapes, Pieces, Components, Topic Maps, and Grids. Create product structures, define document types, build taxonomies, organize catalogue items, design relationships between items, implement classification bridges, configure item relations with shape restrictions, model brand and product colours with the Colors component (hex, RGB, HSL, CMYK, Pantone, RAL, colour presets, swatches), and architect scalable data models. Use when modeling content, creating shapes, defining components, building taxonomies, designing relationship patterns, implementing semantic bridges, configuring product variants, choosing between a Colors component and a Selection for colour, or structuring catalogue hierarchies. metadata: author: Crystallize version: "3.0" diff --git a/use-crystallize/skills/content-model/references/components.md b/use-crystallize/skills/content-model/references/components.md index 982b4c8..1c09efd 100644 --- a/use-crystallize/skills/content-model/references/components.md +++ b/use-crystallize/skills/content-model/references/components.md @@ -68,12 +68,12 @@ Choosing the right component for each piece of **additional** data determines ho ### "I need a controlled set of options" -| Scenario | Component | -| ----------------------------------------------------------- | ------------- | -| Predefined dropdown or multi-select (color, size, material) | **Selection** | -| Boolean flag (featured, in-stock, certified) | **Switch** | -| A date or timestamp | **Datetime** | -| Geographic coordinates | **Location** | +| Scenario | Component | +| --------------------------------------------------------- | ------------- | +| Predefined dropdown or multi-select (size, material, fit) | **Selection** | +| Boolean flag (featured, in-stock, certified) | **Switch** | +| A date or timestamp | **Datetime** | +| Geographic coordinates | **Location** | - **Selection** → dropdown, radio buttons, or checkboxes with predefined options. Each option has a `key` (API value) and `value` (display label). Configure min/max selections to control behavior: - **Radio/Enum pattern**: min=1, max=1, required → exactly one selection, renders as radio buttons or dropdown @@ -84,6 +84,32 @@ Choosing the right component for each piece of **additional** data determines ho - **Datetime** → date with optional time (type: `datetime`). Use for: launch date, preorder availability, expiration, event scheduling. - **Location** → latitude/longitude. Use for: store locator, pickup points, product origin. +### "I need to store a colour" + +| Scenario | Component | +| ----------------------------------------------------------------------- | -------------------------- | +| A brand, product or material colour that has to render as a real swatch | **Colors** | +| A colour _name_ used only for filtering and navigation | **Selection** or **Topic** | + +- **Colors (`colors`)** → stores one or more colour entries. Each entry can carry the same colour in several notations **at once** — `hex`, `rgb`, `hsl`, `cmyk`, `pantone`, `ral` — plus a free-text `label`. The component is a **list**, so a single component holds a whole colourway. + - **Configuration**: `min` / `max` (number of entries), `colorPresets` (a fixed palette editors pick from), `customColorSpaces` (which notations beyond the default are offered), `requireLabel`, plus the usual `required`, `discoverable` and `multilingual`. + - **`colorPresets` is how you enforce a brand palette.** Editors choose from the palette instead of typing hex values, which is the difference between a design system and 40 slightly different greys. + - **`requireLabel: true`** forces every entry to be named ("Midnight Blue"). The label is what the storefront shows; the value is what it renders. + +**Why one entry carries several notations**: screen wants `hex`/`rgb`, print wants `cmyk`/`pantone`, paint and industrial want `ral`. One authoritative entry serves all three. Modelling them as three separate Single Line components guarantees they drift apart the first time a colour is corrected. + +#### Colors vs Selection — the decision people get wrong + +| You need | Use | +| --------------------------------------------------------------- | ---------------------------- | +| The actual colour, to render (swatch, chip, 3D configurator) | **Colors** | +| A stable token to filter, facet and build URLs on | **Selection** or a **Topic** | +| Both — the normal case for fashion, paint, furniture, packaging | **Both**, deliberately | + +Run them side by side: the Selection or topic carries `red` for navigation and filtering, and the Colors component carries `#B22222` plus its Pantone for rendering. + +**Do not filter on colour values.** Shoppers filter on "Red", not on `#B22222`, and two near-identical hexes will not group into one facet. The filterable token and the renderable value are different jobs — give them different components. + ### "I need to group or structure fields" | Scenario | Component | @@ -322,22 +348,23 @@ Every component has an **isTranslatable** flag that determines whether content c Different components have different default `isTranslatable` values based on their typical use cases: -| Component | Default isTranslatable | Typical use case | -| ------------------------ | ---------------------- | ------------------------------------------------------------- | -| **Single Line** | ✓ True | Titles, subtitles, slogans, taglines | -| **Rich Text** | ✓ True | Descriptions, marketing copy, specifications | -| **Paragraph Collection** | ✓ True | Blog posts, landing pages, editorial content | -| **Numeric** | ✗ False | Dimensions, weights, ratings (universal values) | -| **Properties Table** | ✗ False | Technical specs with shared keys/values | -| **Images** | ✗ False | Product photos (same images, translate captions) | -| **Videos** | ✗ False | Product videos (same videos, translate captions) | -| **Files** | ✗ False | Shared downloads (or create separate per-market) | -| **Selection** | ✗ False | Shared options (size, color codes) | -| **Switch** | ✗ False | Boolean flags (universal true/false) | -| **Datetime** | ✗ False | Universal dates (launch, expiration) | -| **Location** | ✗ False | Geographic coordinates (universal) | -| **Item Relation** | ✗ False | Relationships are shared, but related items can be translated | -| **Grid Relation** | ✗ False | Grid references shared, grid content can be translated | +| Component | Default isTranslatable | Typical use case | +| ------------------------ | ---------------------- | -------------------------------------------------------------- | +| **Single Line** | ✓ True | Titles, subtitles, slogans, taglines | +| **Rich Text** | ✓ True | Descriptions, marketing copy, specifications | +| **Paragraph Collection** | ✓ True | Blog posts, landing pages, editorial content | +| **Numeric** | ✗ False | Dimensions, weights, ratings (universal values) | +| **Properties Table** | ✗ False | Technical specs with shared keys/values | +| **Images** | ✗ False | Product photos (same images, translate captions) | +| **Videos** | ✗ False | Product videos (same videos, translate captions) | +| **Files** | ✗ False | Shared downloads (or create separate per-market) | +| **Selection** | ✗ False | Shared options (size, colour codes) | +| **Colors** | ✗ False | Colour values are universal — `label` is the translatable part | +| **Switch** | ✗ False | Boolean flags (universal true/false) | +| **Datetime** | ✗ False | Universal dates (launch, expiration) | +| **Location** | ✗ False | Geographic coordinates (universal) | +| **Item Relation** | ✗ False | Relationships are shared, but related items can be translated | +| **Grid Relation** | ✗ False | Grid references shared, grid content can be translated | **Important**: These are defaults when adding components. You can override them based on your specific use case. @@ -636,6 +663,9 @@ Is the data a reference to another item? → Yes, to catalogue items → Item Relation → Yes, to a curated grid → Grid Relation +Is the data a colour that has to render? + → Yes → Colors (add a Selection or topic alongside it if you also need to filter by colour) + Is the data a choice from fixed options? → Yes, string labels → Selection → Yes, true/false → Switch @@ -676,11 +706,16 @@ The **discoverable** flag determines whether a component is **indexed in the Dis **Examples:** - Numeric components (price, weight, dimensions) → enables range filters -- Selection components (color, size, material) → enables faceted filtering +- Selection components (colour, size, material) → enables faceted filtering - Single Line (model number, GTIN) → enables exact-match filtering - Switch (featured, on-sale) → enables boolean filtering - Item Relations (brand, category) → enables filtering by related items +**Colors is the exception — do not reach for `discoverable` to build a colour filter.** Filter on the +Selection or topic that carries the colour _name_, and keep the Colors component for the value you +render. A hex is a poor facet: shoppers pick "Red", not `#B22222`, and two near-identical hexes will +not collapse into one bucket. See "Colors vs Selection" above. + **Do NOT mark as discoverable when:** - Data is for internal use only and should not be exposed in public APIs (external system IDs, integration keys, internal workflow flags) @@ -761,12 +796,13 @@ Every component type supports validation rules to enforce data integrity and gui ### Selection Components -| Component | Available validations | -| ------------- | ------------------------------------------ | -| **Selection** | Min/max selections, required/optional | -| **Switch** | No validations (always boolean true/false) | -| **Datetime** | Min/max date range, required/optional | -| **Location** | Required/optional | +| Component | Available validations | +| ------------- | --------------------------------------------------------------------------------------------- | +| **Selection** | Min/max selections, required/optional | +| **Switch** | No validations (always boolean true/false) | +| **Datetime** | Min/max date range, required/optional | +| **Location** | Required/optional | +| **Colors** | Min/max number of entries, require label, restrict available colour spaces, required/optional | **Example use cases:** @@ -775,6 +811,9 @@ Every component type supports validation rules to enforce data integrity and gui - **Required multi-select**: min=1, max=3 → "Select 1-3 colors" — at least one required, up to 3 allowed - **Preselected defaults**: Set `isPreselected: true` on commonly chosen options to prefill the selection for editors. Useful for default sizes, standard shipping modes, or common categories. Multiple options can be preselected. - Date range: launch date must be in the future, expiration within 2 years +- **Colors with min=1, max=1, `requireLabel: true`**: exactly one named brand colour per item +- **Colors with `colorPresets`**: editors can only choose from the approved palette +- **Colors with `customColorSpaces: ["hex", "pantone"]`**: a print-facing shape that wants screen and press values, and nothing else ### Relationship Components diff --git a/use-crystallize/skills/content-model/references/create-shape-api.md b/use-crystallize/skills/content-model/references/create-shape-api.md index ee5930f..b2ca120 100644 --- a/use-crystallize/skills/content-model/references/create-shape-api.md +++ b/use-crystallize/skills/content-model/references/create-shape-api.md @@ -71,7 +71,7 @@ Used in both `components` and `variantComponents` arrays, and recursively inside } ``` -**`ComponentType` enum:** `boolean` | `componentChoice` | `componentMultipleChoice` | `contentChunk` | `datetime` | `files` | `gridRelations` | `images` | `itemRelations` | `location` | `numeric` | `paragraphCollection` | `piece` | `propertiesTable` | `richText` | `selection` | `singleLine` | `videos` +**`ComponentType` enum:** `boolean` | `colors` | `componentChoice` | `componentMultipleChoice` | `contentChunk` | `datetime` | `files` | `gridRelations` | `images` | `itemRelations` | `location` | `numeric` | `paragraphCollection` | `piece` | `propertiesTable` | `richText` | `selection` | `singleLine` | `videos` --- @@ -268,6 +268,39 @@ config: { --- +### colors + +```typescript +config: { + colors: { + required?: Boolean + discoverable?: Boolean + multilingual?: Boolean + min?: Int // min number of colour entries + max?: Int // max number of colour entries + requireLabel?: Boolean // force every entry to carry a label + colorPresets?: { // fixed palette editors pick from + label?: String + hex?: String + rgb?: { r: Int!, g: Int!, b: Int!, a?: Float } + hsl?: { h: Int!, s: Int!, l: Int!, a?: Float } + cmyk?: { c: Int!, m: Int!, y: Int!, k: Int! } + pantone?: String + ral?: String + }[] + customColorSpaces?: ColorSpace[] // cmyk | hex | hsl | pantone | ral | rgb + } +} +``` + +Every field is optional — `colors` with no config is valid. One colour entry may carry several +notations at once, so `colorPresets` entries and stored content share the same shape. + +Use `colorPresets` to lock a shape to a brand palette, and `customColorSpaces` to narrow which +notations editors are offered — a print-facing shape might expose only `hex` and `pantone`. + +--- + ### propertiesTable ```typescript diff --git a/use-crystallize/skills/data-creation/SKILL.md b/use-crystallize/skills/data-creation/SKILL.md index 00d2c81..0bb0554 100644 --- a/use-crystallize/skills/data-creation/SKILL.md +++ b/use-crystallize/skills/data-creation/SKILL.md @@ -139,19 +139,19 @@ When assigning locations to items: When generating data, match component types exactly: -| Type | How to format | -| -------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `singleLine` | Short text strings. | -| `richText` | Plain text content (no HTML tags). | -| `numeric` | Number (integer or decimal). When the shape shows `[units: ...]`, use an object with the EXACT unit string from the list: `{ "value": 25, "unit": "cm" }`. The unit MUST be one of the configured units — do NOT invent units or use synonyms (e.g., if units are `["in"]`, use `"in"` NOT `"inch"`/`"inches"`). | -| `boolean` | `true` / `false`. | -| `datetime` | ISO 8601 date string. | -| `images` | Array of image URLs/keys, or a single URL string for the value. | -| `itemRelations` | Catalogue path (e.g., `/brands/nike`) — MUST be a path string starting with `/`, NOT a plain name. Convert names to paths: `Fender` → `/brands/fender`, `Nike` → `/brands/nike`. Check the "Available Catalogue Paths" section for valid paths. Paths that exist show GREEN; invalid paths show RED. | -| `selection` | MUST be an EXACT key from the options shown in `[options: ...]`. Do NOT invent values. E.g., if options are `["solid", "semi-hollow", "hollow", "acoustic"]`, use `"solid"` NOT `"solid-body"`. If options are `["6", "7", "8", "12"]`, use `"6"` NOT `"6-string"`. If no option fits, pick the closest match. | -| `contentChunk` | Use **dot notation** for sub-components (see below). | -| `piece` | Use **dot notation** for sub-components (see below). | -| `propertiesTable`, `paragraphCollection`, `componentChoice`, `componentMultipleChoice` | **Skip** — do NOT generate values for these complex types. | +| Type | How to format | +| ------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `singleLine` | Short text strings. | +| `richText` | Plain text content (no HTML tags). | +| `numeric` | Number (integer or decimal). When the shape shows `[units: ...]`, use an object with the EXACT unit string from the list: `{ "value": 25, "unit": "cm" }`. The unit MUST be one of the configured units — do NOT invent units or use synonyms (e.g., if units are `["in"]`, use `"in"` NOT `"inch"`/`"inches"`). | +| `boolean` | `true` / `false`. | +| `datetime` | ISO 8601 date string. | +| `images` | Array of image URLs/keys, or a single URL string for the value. | +| `itemRelations` | Catalogue path (e.g., `/brands/nike`) — MUST be a path string starting with `/`, NOT a plain name. Convert names to paths: `Fender` → `/brands/fender`, `Nike` → `/brands/nike`. Check the "Available Catalogue Paths" section for valid paths. Paths that exist show GREEN; invalid paths show RED. | +| `selection` | MUST be an EXACT key from the options shown in `[options: ...]`. Do NOT invent values. E.g., if options are `["solid", "semi-hollow", "hollow", "acoustic"]`, use `"solid"` NOT `"solid-body"`. If options are `["6", "7", "8", "12"]`, use `"6"` NOT `"6-string"`. If no option fits, pick the closest match. | +| `contentChunk` | Use **dot notation** for sub-components (see below). | +| `piece` | Use **dot notation** for sub-components (see below). | +| `propertiesTable`, `paragraphCollection`, `componentChoice`, `componentMultipleChoice`, `colors` | **Skip** — do NOT generate values for these complex types. `colors` entries are structured colour objects, not strings; author them through the API instead. | **IMPORTANT:** Both `selection` keys AND `numeric` units MUST exactly match the configured values shown in `[options: ...]` and `[units: ...]`. Mismatches cause deployment failures. @@ -172,7 +172,7 @@ For `contentChunk` and `piece` components that have sub-components listed in the Format: `"parentId.subComponentId": value`. Each sub-component gets its own column in the grid. -**SKIP truly complex types**: `propertiesTable`, `paragraphCollection`, `componentChoice`, `componentMultipleChoice` — do NOT generate values for these. +**SKIP truly complex types**: `propertiesTable`, `paragraphCollection`, `componentChoice`, `componentMultipleChoice`, `colors` — do NOT generate values for these. ## Product Image Handling (CRITICAL for spreadsheet imports) @@ -221,7 +221,7 @@ Before emitting a `json:itemData` block, verify: - [ ] `itemRelations` values are paths starting with `/`, not plain names. - [ ] Spreadsheet location columns are combined into a single `/a/b/c` path — never `/`. - [ ] Product image URLs go to `_defaultVariantImage` (or to the variant's `image` field for variants). -- [ ] Complex types (`propertiesTable`, `paragraphCollection`, `componentChoice`, `componentMultipleChoice`) are skipped. +- [ ] Complex types (`propertiesTable`, `paragraphCollection`, `componentChoice`, `componentMultipleChoice`, `colors`) are skipped. - [ ] Spreadsheet imports emit one item per row; demo data emits 5-10 unless asked otherwise. ## Common Mistakes diff --git a/use-crystallize/skills/mutation/references/core-api.md b/use-crystallize/skills/mutation/references/core-api.md index 1f9f3e3..d1575fc 100644 --- a/use-crystallize/skills/mutation/references/core-api.md +++ b/use-crystallize/skills/mutation/references/core-api.md @@ -285,6 +285,42 @@ mutation UpdateSelection { } ``` +### Colors + +```graphql +mutation UpdateColors { + item { + updateComponent( + itemId: "item-id" + language: "en" + component: { + componentId: "brand-color" + colors: { + colors: [ + { label: "Midnight Blue", hex: "#191970", rgb: { r: 25, g: 25, b: 112 }, pantone: "2758 C" } + ] + } + } + ) { + ... on Item { + id + } + } + } +} +``` + +The content input is `colors: { colors: [GraphqlInputColorEntry!] }` — note the doubled key: the +component input field is `colors`, and it wraps a list also called `colors`. + +Each entry carries any combination of `hex`, `rgb { r g b a }`, `hsl { h s l a }`, +`cmyk { c m y k }`, `pantone`, `ral` and `label`. **They are notations of the same colour, not +separate colours** — send as many as the item actually has, and write the whole list every time, since +the list replaces rather than merges. `colors` is also valid inside `NestableComponentInput`, so it +works within chunks, choices and pieces. + +See the [[content-model]] skill for when to use Colors rather than a Selection. + ### Item Relations ```graphql diff --git a/use-crystallize/skills/query/references/catalogue-api.md b/use-crystallize/skills/query/references/catalogue-api.md index 91637df..f7b6f8d 100644 --- a/use-crystallize/skills/query/references/catalogue-api.md +++ b/use-crystallize/skills/query/references/catalogue-api.md @@ -102,6 +102,46 @@ For search, filtering, and faceting, use the Discovery API instead. } ``` +### Reading a Colors component + +A `colors` component returns `ColorsContent`, holding a list of entries. Each entry carries whichever +notations were stored — they are the **same colour** expressed several ways, so select the ones your +frontend actually renders rather than all of them. + +```graphql +{ + catalogue(language: "en", path: "/shop/furniture/dining-chair") { + name + components { + id + content { + ... on ColorsContent { + colors { + label + hex + rgb { + r + g + b + a + } + pantone + ral + } + } + } + } + } +} +``` + +`ColorEntry` exposes `label`, `hex`, `rgb { r g b a }`, `hsl { h s l a }`, `cmyk { c m y k }`, +`pantone` and `ral`. Every field is nullable — an entry only carries the notations that were authored, +so a storefront reading `hex` needs a fallback for entries stored as Pantone only. + +Colour **filtering** does not belong here or in Discovery; filter on the Selection or topic that +carries the colour name. See [[content-model]] for the modelling rule. + ## cURL Example ```bash