-
Notifications
You must be signed in to change notification settings - Fork 142
Implement custom cardstack tag colors and order #2809
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Implement custom cardstack tag colors and order #2809
Conversation
- Add <tags> syntax parsing in MdAttributeRenderer to extract tag configurations - Implement processCardStackAttributes() to parse <tag> elements with name and color - Add tagConfigs and dataTagConfigs props to CardStack component - Implement tag ordering algorithm that respects custom config order - Add color normalization supporting both hex colors and Bootstrap color names - Update badge rendering with conditional styling (Bootstrap classes vs inline styles) - Add isBootstrapColor() and getTextColor() helper methods for color handling - Apply same color logic to Card component for visual consistency - Add comprehensive test coverage for custom tag ordering and colors - Update documentation with examples for hex colors and Bootstrap color names - Document new <tags> and <tag> element syntax and options
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #2809 +/- ##
==========================================
+ Coverage 71.83% 72.19% +0.36%
==========================================
Files 134 135 +1
Lines 7331 7449 +118
Branches 1622 1548 -74
==========================================
+ Hits 5266 5378 +112
- Misses 1937 1943 +6
Partials 128 128 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
@yihao03 those 'unlisted' tags appear in default order but after the listed tags? Or they appear mixed together with 'listed' tags? |
|
@damithc they will appear after the specified tags |
Harjun751
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Commenting on some minor code quality nits here. Code reads good!
|
|
||
| The example above also illustrates how to use the `keywords` attribute to specify additional search terms for a card. | ||
|
|
||
| ### Custom Tag Order and Colors |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Minor nit: I wonder if it'd be better (as a reader) for the custom tag order to be above the MRQ cardstack example?
I think I'd prefer it that way but I realize that it might be subjective so if you think this is better/ok that's fine!
| // Parse each <tag> element | ||
| if (tagsNode.children) { | ||
| tagsNode.children.forEach((child) => { | ||
| if (child.type === 'tag' && (child as MbNode).name === 'tag') { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I notice this check occurs in two places. Would it be better to use a type predicate?
As an added bonus, you wouldn't need to do the type cast on the next line, and your intention would be clearer (calling "isANodeWithName()" over "child.type === ...")
| } | ||
|
|
||
| // Remove the <tags> node from the DOM tree | ||
| node.children.splice(tagsNodeIndex, 1); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wonder if it would be possible to apply display: none on cardstack > tags elements to hide them instead of manipulating the DOM directly? Though I'm assuming there's some reason/case here that I'm not considering haha.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You know what, after thinking about this I think the better solution is to remove it from the DOM like you did. Better to do the processing on the server anyway, and screen readers wouldn't see the element.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wow, didn't imagine it'd be this hard to get the colors like that. Cool solution.
|
|
||
| You can customize the order and colors of tags by using a `<tags>` element inside the `cardstack`: | ||
|
|
||
| <include src="codeAndOutputSeparate.md" boilerplate > |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe you could consider using `src="codeAndOutput.md"!
<include src="codeAndOutput.md" boilerplate>
<variable name="highlightStyle">html</variable>
<variable name="code">
Only need to include once if the code is the same
</variable>
</include>
You could also replace the top example (the existing docs example) with the following as well to incoporate better reuse:
<include src="codeAndOutput.md" boilerplate >
<variable name="highlightStyle">html</variable>
<variable name="code">
<cardstack searchable>
<card header="**Winston Churchill**" tag="Success, Perseverance">
Success is not final, failure is not fatal: it is the courage to continue that counts
</card>
<card header="**Albert Einstein**" tag="Success, Perseverance">
In the middle of every difficulty lies opportunity
</card>
<card header="**Theodore Roosevelt**" tag="Motivation, Hard Work">
Do what you can, with what you have, where you are
</card>
<card header="**Steve Jobs**" tag="Happiness, Mindset">
Your time is limited, so don’t waste it living someone else’s life
</card>
</cardstack>
</span>
</variable>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR adds support for custom tag ordering and colors in the CardStack component, wired end‑to‑end from Markdown syntax through the core HTML processor to the Vue UI, with supporting utility functions and documentation.
Changes:
- Introduced shared color utilities (
BADGE_COLOURS,isBootstrapColor,getTextColor,normalizeColor,MIN_TAGS_FOR_SELECT_ALL) and HTML escape helpers for both core and Vue packages. - Extended
CardStackandCardVue components to consume serialized tag configuration metadata (data-tag-configs/tagConfigs) and render tags in custom order with either Bootstrap or hex colors. - Updated the core Markdown/HTML processing pipeline (
MdAttributeRenderer,NodeProcessor) to parse<tags><tag .../></tags>blocks incardstack, serialize them into a safely escapeddata-tag-configsattribute, and documented the feature with examples and tests.
Reviewed changes
Copilot reviewed 13 out of 13 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
packages/vue-components/src/utils/utils.js |
Adds escapeHTML/unescapeHTML helpers for safely handling serialized JSON in attributes on the Vue side, mirroring the core escape behavior. |
packages/vue-components/src/utils/colors.js |
Introduces reusable badge color utilities (Bootstrap mapping, contrast-aware text color, normalization) used by CardStack/Card. |
packages/vue-components/src/cardstack/CardStack.vue |
Refactors tag color constants into shared utilities, adds tagConfigs/dataTagConfigs props, parses and applies custom tag order/colors to tagMapping, and adjusts rendering to support both Bootstrap and hex colors. |
packages/vue-components/src/cardstack/Card.vue |
Updates tag badge rendering to use the new color utilities so individual cards reflect the configured tag colors consistently. |
packages/vue-components/src/__tests__/utils.spec.js |
Adds unit tests for the Vue escapeHTML/unescapeHTML functions, including JSON and entity round‑trip behavior. |
packages/vue-components/src/__tests__/colors.spec.js |
Covers BADGE_COLOURS, MIN_TAGS_FOR_SELECT_ALL, isBootstrapColor, getTextColor, and normalizeColor to ensure color handling stays consistent. |
packages/vue-components/src/__tests__/CardStack.spec.js |
Extends CardStack tests to verify custom tag ordering, hex and Bootstrap color application, fallback behavior for unconfigured tags, and robustness to invalid tag configs. |
packages/core/test/unit/utils/escape.test.ts |
Adds tests for the core escapeHTML implementation ensuring correct escaping of special characters and entities. |
packages/core/test/unit/html/MdAttributeRenderer.test.ts |
Introduces tests around processCardStackAttributes for parsing <tags> blocks, building data-tag-configs, escaping values correctly, and removing the <tags> node. |
packages/core/src/utils/escape.ts |
Implements the core escapeHTML function used when serializing tag configuration JSON into HTML-safe attributes. |
packages/core/src/html/NodeProcessor.ts |
Hooks cardstack nodes into the attribute-processing pipeline by delegating to MdAttributeRenderer.processCardStackAttributes. |
packages/core/src/html/MdAttributeRenderer.ts |
Adds processCardStackAttributes to extract <tag> definitions under <tags>, build a typed config array, escape it, and set data-tag-configs on the cardstack node. |
docs/userGuide/syntax/cardstacks.md |
Documents the new <tags>/<tag> syntax, explains ordering and color options (hex and Bootstrap names), and adds illustrative examples with rendered output. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const customConfigMap = new Map(); | ||
| customConfigs.forEach((config) => { | ||
| customConfigMap.set(config.name, config); | ||
| }); | ||
|
|
Copilot
AI
Jan 30, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
customConfigMap is constructed and populated here but never used, which adds unnecessary complexity to updateTagMapping and may confuse future readers. Since the logic below operates directly on customConfigs and tagMap, you can safely remove customConfigMap (and its population loop) unless there is a planned future use, in which case it should be wired into the lookup logic now.
| const customConfigMap = new Map(); | |
| customConfigs.forEach((config) => { | |
| customConfigMap.set(config.name, config); | |
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yes
gerteck
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks @yihao03 for this PR, great work! Added some comments
| has, | ||
| }; | ||
|
|
||
| export type tagAttribs = { name: string; color?: string }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
small nit, maybe rename to CardStackTagAttribs or CardStackTagConfig (pascal case)
| searchTerms: [], | ||
| selectedTags: [], | ||
| searchData: new Map(), | ||
| component: null, // Will be set to the component instance |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Creating Circular Reference, stores the component instance (this) inside its own data property (cardStackRef). This creates a circular reference which can cause issues with memory leaks or debugging. (For garbage collection etc.)
-
Data objects should hold data. Methods that operate on the component's state (like accessing this.dataTagConfigs props) should be standard Vue methods, not functions attached to a data object.
Refactor -
Consider moving
updateTagMappingto the component's methods section, then it can directly access this.cardStackRef and this.tagConfigs directly without needing component pointer.
| const customConfigMap = new Map(); | ||
| customConfigs.forEach((config) => { | ||
| customConfigMap.set(config.name, config); | ||
| }); | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yes
| // The prop might be double-stringified, so parse once or twice | ||
| let parsed = decodedConfig; | ||
| // eslint-disable-next-line lodash/prefer-lodash-typecheck | ||
| for (let i = 0; i < 2 && typeof parsed === 'string'; i += 1) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
do we need this double loop? When would the prop be double-stringified, and why?
| customConfigs = parsed; | ||
| } | ||
| } catch (e) { | ||
| // If parsing fails, continue with default behavior |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the code seems explanatory enough without the comment here!
| }; | ||
| }, | ||
| created() { | ||
| this.cardStackRef.component = this; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
creates a circular reference, which can cause memory leaks
What is the purpose of this pull request?
Overview of changes:
Implemented custom tag ordering and color in the CardStack component as mentioned in issue #2783.
Users are now able to include:
to their cardstack. Note that users are not required to include all tags in the custom order; any tags not specified will be appended in default order.
Colors can be specified in hex format or bootstrap color names.
Anything you'd like to highlight/discuss:
Testing instructions:
markbind serve -dProposed commit message: (wrap lines at 72 characters)
Enable custom tag order and colors in CardStack component
Checklist: ☑️
Reviewer checklist:
Indicate the SEMVER impact of the PR:
At the end of the review, please label the PR with the appropriate label:
r.Major,r.Minor,r.Patch.Breaking change release note preparation (if applicable):