-
Notifications
You must be signed in to change notification settings - Fork 249
feat(color-loupe): migrate sp-color-loupe component to 2nd-gen #6147
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
Changes from 11 commits
32a62b0
257afe2
c07e84e
758d969
688e358
087c548
067bbe6
cffaead
50b7660
eb74d4b
e207c22
fc8dbcf
474e315
7acd6ea
5fd30c1
05cafaf
0e20e5f
11f1742
2a706e5
2599274
e360348
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,47 @@ | ||
| /** | ||
| * Copyright 2026 Adobe. All rights reserved. | ||
| * This file is licensed to you under the Apache License, Version 2.0 (the "License"); | ||
| * you may not use this file except in compliance with the License. You may obtain a copy | ||
| * of the License at http://www.apache.org/licenses/LICENSE-2.0 | ||
| * | ||
| * Unless required by applicable law or agreed to in writing, software distributed under | ||
| * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS | ||
| * OF ANY KIND, either express or implied. See the License for the specific language | ||
| * governing permissions and limitations under the License. | ||
| */ | ||
|
|
||
| import { property } from 'lit/decorators.js'; | ||
|
|
||
| import { SpectrumElement } from '@spectrum-web-components/core/element/index.js'; | ||
|
|
||
| import { COLOR_LOUPE_DEFAULT_COLOR } from './ColorLoupe.types.js'; | ||
|
|
||
| /** | ||
| * A visual magnifier that shows the currently picked color, including | ||
| * transparency over an opacity checkerboard, inside a loupe shape. | ||
| * | ||
| * The loupe is not an interactive control — accessibility semantics are | ||
| * provided by the parent color picker / color field. | ||
| * | ||
| * @element swc-color-loupe | ||
| */ | ||
| export abstract class ColorLoupeBase extends SpectrumElement { | ||
| // ───────────────── | ||
| // SHARED API | ||
| // ───────────────── | ||
|
|
||
| /** | ||
| * Whether the loupe is visible. When `false` the loupe is hidden via | ||
| * CSS opacity and transform transitions. | ||
| */ | ||
| @property({ type: Boolean, reflect: true }) | ||
| public open = false; | ||
|
|
||
| /** | ||
| * The CSS color value to display inside the loupe. | ||
| * Supports any valid CSS color string, including those with alpha | ||
| * transparency (which reveals the checkerboard behind). | ||
| */ | ||
| @property({ type: String }) | ||
| public color = COLOR_LOUPE_DEFAULT_COLOR; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| /** | ||
| * Copyright 2026 Adobe. All rights reserved. | ||
| * This file is licensed to you under the Apache License, Version 2.0 (the "License"); | ||
| * you may not use this file except in compliance with the License. You may obtain a copy | ||
| * of the License at http://www.apache.org/licenses/LICENSE-2.0 | ||
| * | ||
| * Unless required by applicable law or agreed to in writing, software distributed under | ||
| * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS | ||
| * OF ANY KIND, either express or implied. See the License for the specific language | ||
| * governing permissions and limitations under the License. | ||
| */ | ||
|
|
||
| /** | ||
| * Default color value for a newly created color loupe. | ||
| * Semi-transparent red allows the opacity checkerboard to show through. | ||
| */ | ||
| export const COLOR_LOUPE_DEFAULT_COLOR = 'rgba(255, 0, 0, 0.5)'; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this isnt a type and can we defined in the base since its just a default value
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good catch — you're right, it wasn't a type. Since @property({ type: String })
public color = 'rgba(255, 0, 0, 0.5)';A short line in the property JSDoc explains the choice (semi-transparent red reveals the opacity checkerboard on initial render). Also updated the |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| /** | ||
| * Copyright 2026 Adobe. All rights reserved. | ||
| * This file is licensed to you under the Apache License, Version 2.0 (the "License"); | ||
| * you may not use this file except in compliance with the License. You may obtain a copy | ||
| * of the License at http://www.apache.org/licenses/LICENSE-2.0 | ||
| * | ||
| * Unless required by applicable law or agreed to in writing, software distributed under | ||
| * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS | ||
| * OF ANY KIND, either express or implied. See the License for the specific language | ||
| * governing permissions and limitations under the License. | ||
| */ | ||
| export * from './ColorLoupe.base.js'; | ||
| export * from './ColorLoupe.types.js'; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,85 @@ | ||
| /** | ||
| * Copyright 2026 Adobe. All rights reserved. | ||
| * This file is licensed to you under the Apache License, Version 2.0 (the "License"); | ||
| * you may not use this file except in compliance with the License. You may obtain a copy | ||
| * of the License at http://www.apache.org/licenses/LICENSE-2.0 | ||
| * | ||
| * Unless required by applicable law or agreed to in writing, software distributed under | ||
| * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS | ||
| * OF ANY KIND, either express or implied. See the License for the specific language | ||
| * governing permissions and limitations under the License. | ||
| */ | ||
|
|
||
| import { CSSResultArray, html, TemplateResult } from 'lit'; | ||
| import { classMap } from 'lit/directives/class-map.js'; | ||
|
|
||
| import { ColorLoupeBase } from '@spectrum-web-components/core/components/color-loupe'; | ||
|
|
||
| import styles from './color-loupe.css'; | ||
|
|
||
| /** | ||
| * A visual magnifier that displays the currently picked color inside a | ||
| * loupe-shaped container with an opacity checkerboard behind transparent | ||
| * colors. The loupe is a non-interactive, visual-only companion to | ||
| * color selection controls such as `<swc-color-field>`. | ||
| * | ||
| * @element swc-color-loupe | ||
| * @status preview | ||
| * @since 0.0.1 | ||
| * | ||
| * @example | ||
| * <swc-color-loupe open color="rgba(0, 128, 255, 0.7)"></swc-color-loupe> | ||
| */ | ||
| export class ColorLoupe extends ColorLoupeBase { | ||
| // ────────────────────────────── | ||
| // RENDERING & STYLING | ||
| // ────────────────────────────── | ||
|
|
||
| // TODO: Migrate opacity-checkerboard to 2nd gen and consume it here; checkerboard styling is currently hardcoded in color-loupe.css. | ||
|
caseyisonit marked this conversation as resolved.
Outdated
|
||
|
|
||
| public static override get styles(): CSSResultArray { | ||
| return [styles]; | ||
| } | ||
|
|
||
| protected override render(): TemplateResult { | ||
| return html` | ||
| <div | ||
| class=${classMap({ | ||
| ['swc-ColorLoupe']: true, | ||
| })} | ||
| > | ||
| <div class="swc-ColorLoupe-checkerboard swc-ColorLoupe--clipped"></div> | ||
| <div | ||
| class="swc-ColorLoupe-colorFill swc-ColorLoupe--clipped" | ||
| style="background: ${this.color}" | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we need to add a comment in the contributor docs stating to pass only CSS only strings from trusted sources.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good call. Added a new section "Inline CSS strings from component properties" to Using this
It also includes ✅ preferred / ✅ acceptable /
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The second example it provides of passing this into the value of a custom property vs straight to the
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Implemented. The template now routes // ColorLoupe.ts
style=${styleMap({
'--swc-color-loupe-picked-color': this.color,
})}/* color-loupe.css */
.swc-ColorLoupe-colorFill {
background: var(--swc-color-loupe-picked-color);
}On HSV point — both |
||
| ></div> | ||
| <svg aria-hidden="true" class="swc-ColorLoupe-svg" overflow="visible"> | ||
|
blunteshwar marked this conversation as resolved.
|
||
| <defs> | ||
| <path | ||
| id="loupe-path" | ||
| d="M 22 60 C 18.2 56 14.6 51.7 11.3 47.2 C 8.3 43.3 5.7 39.1 3.5 34.7 C 1.2 30 0 25.9 0 22.4 C 0 17.2 1.8 12.2 5 8.2 C 8.2 4.2 12.7 1.5 17.6 0.4 C 22.6 -0.6 27.8 0.2 32.3 2.6 C 36.8 5 40.3 8.9 42.3 13.7 C 43.4 16.4 44 19.4 44 22.4 C 44 25.9 42.8 30 40.5 34.7 C 38.3 39.1 35.7 43.3 32.7 47.3 C 29.4 51.7 25.8 56 22 60 Z" | ||
| transform="translate(2, 2)" | ||
| /> | ||
| <mask id="loupe-mask"> | ||
| <rect x="0" y="0" height="100" width="100" fill="white" /> | ||
| <use href="#loupe-path" fill="black" /> | ||
| </mask> | ||
| </defs> | ||
|
|
||
| <g class="swc-ColorLoupe-loupe"> | ||
| <use | ||
| href="#loupe-path" | ||
| mask="url(#loupe-mask)" | ||
| class="swc-ColorLoupe-innerBorder" | ||
| /> | ||
| <use | ||
| href="#loupe-path" | ||
| mask="url(#loupe-mask)" | ||
| class="swc-ColorLoupe-outerBorder" | ||
| /> | ||
| </g> | ||
| </svg> | ||
| </div> | ||
| `; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,102 @@ | ||
| /** | ||
| * Copyright 2026 Adobe. All rights reserved. | ||
| * This file is licensed to you under the Apache License, Version 2.0 (the "License"); | ||
| * you may not use this file except in compliance with the License. You may obtain a copy | ||
| * of the License at http://www.apache.org/licenses/LICENSE-2.0 | ||
| * | ||
| * Unless required by applicable law or agreed to in writing, software distributed under | ||
| * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS | ||
| * OF ANY KIND, either express or implied. See the License for the specific language | ||
| * governing permissions and limitations under the License. | ||
| */ | ||
|
|
||
|
blunteshwar marked this conversation as resolved.
|
||
| :host { | ||
| display: block; | ||
| position: absolute; | ||
| inset-block-end: calc((token("color-handle-size") - token("color-handle-outer-border-width")) + var(--swc-color-loupe-offset, token("color-loupe-bottom-to-color-handle"))); | ||
| inset-inline-end: calc(50% - (token("color-loupe-width") / 2)); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done. Defined once as a private property on :host {
--_swc-color-loupe-width: token("color-loupe-width");
...
inline-size: var(--_swc-color-loupe-width);
inset-inline-end: calc(50% - (var(--_swc-color-loupe-width) / 2));
}
:host(:dir(rtl)) {
inset-inline-end: calc(50% - (var(--_swc-color-loupe-width) / 2) - 1px);
} |
||
| } | ||
|
|
||
| * { | ||
| box-sizing: border-box; | ||
| } | ||
|
|
||
| .swc-ColorLoupe { | ||
| --_swc-color-loupe-opacity: var(--swc-color-loupe-opacity, 0); | ||
| --_swc-color-loupe-transform: var(--swc-color-loupe-transform, translateY(var(--swc-color-loupe-animation-distance, 8px))); | ||
|
|
||
| position: relative; | ||
| inline-size: token("color-loupe-width"); | ||
| block-size: token("color-loupe-height"); | ||
| pointer-events: none; | ||
| opacity: var(--_swc-color-loupe-opacity); | ||
| filter: drop-shadow(var(--swc-color-loupe-drop-shadow-x, token("drop-shadow-elevated-x")) var(--swc-color-loupe-drop-shadow-y, token("drop-shadow-elevated-y")) var(--swc-color-loupe-drop-shadow-blur, token("drop-shadow-elevated-blur")) var(--swc-color-loupe-drop-shadow-color, token("drop-shadow-elevated-color"))); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Per our guidelines, we are not exposing every token, only those that are actually modified by the component. This definition can be simplified to remove all of the exposed properties. Same for several other instances in this stylesheet. I assume AI just converted the mod properties, but again, that is an anti-pattern.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done. Exposed Collapsed to direct
The The outer-border-color is only modified in forced-colors mode, so per the exclusions guideline it's now an internal |
||
| transform: var(--_swc-color-loupe-transform); | ||
| transform-origin: bottom center; | ||
| transition: | ||
| transform 100ms ease-in-out, | ||
| opacity 125ms ease-in-out; | ||
| } | ||
|
|
||
| /* Opacity checkerboard shown behind transparent picked colors. | ||
| The dark-square token has separate light/dark theme values, so | ||
| we use light-dark() with the explicit theme variants. */ | ||
| .swc-ColorLoupe-checkerboard { | ||
| position: absolute; | ||
| inset-block-start: 2px; | ||
| inset-inline-start: 2px; | ||
| inline-size: 100%; | ||
| block-size: 100%; | ||
| background: repeating-conic-gradient(light-dark(var(--swc-opacity-checkerboard-square-dark-light), var(--swc-opacity-checkerboard-square-dark-dark)) 0% 25%, token("opacity-checkerboard-square-light") 0% 50%) 0 0 / token("opacity-checkerboard-square-size-medium") token("opacity-checkerboard-square-size-medium"); | ||
| } | ||
|
|
||
| /* Color fill layer — displays the picked color */ | ||
| .swc-ColorLoupe-colorFill { | ||
| position: absolute; | ||
| inset-block-start: 2px; | ||
| inset-inline-start: 2px; | ||
| inline-size: 100%; | ||
| block-size: 100%; | ||
| } | ||
|
|
||
| /* Clip to the loupe teardrop shape */ | ||
| .swc-ColorLoupe--clipped { | ||
| clip-path: path("M 22 60 C 18.2 56 14.6 51.7 11.3 47.2 C 8.3 43.3 5.7 39.1 3.5 34.7 C 1.2 30 0 25.9 0 22.4 C 0 17.2 1.8 12.2 5 8.2 C 8.2 4.2 12.7 1.5 17.6 0.4 C 22.6 -0.6 27.8 0.2 32.3 2.6 C 36.8 5 40.3 8.9 42.3 13.7 C 43.4 16.4 44 19.4 44 22.4 C 44 25.9 42.8 30 40.5 34.7 C 38.3 39.1 35.7 43.3 32.7 47.3 C 29.4 51.7 25.8 56 22 60 Z"); | ||
| } | ||
|
|
||
| /* SVG overlay — inherits host dimensions */ | ||
| .swc-ColorLoupe-svg { | ||
| position: absolute; | ||
| inline-size: inherit; | ||
| block-size: inherit; | ||
| } | ||
|
|
||
| /* Inner border: filled with the picked color + thin stroke */ | ||
| .swc-ColorLoupe-innerBorder { | ||
| fill: none; | ||
| stroke: var(--swc-color-loupe-inner-border-color, token("color-loupe-inner-border")); | ||
| stroke-width: var(--swc-color-loupe-inner-border-width, token("color-loupe-inner-border-width")); | ||
| } | ||
|
|
||
| /* Outer border: unfilled with wider stroke */ | ||
| .swc-ColorLoupe-outerBorder { | ||
| fill: none; | ||
| stroke: var(--swc-color-loupe-outer-border-color, token("color-loupe-outer-border")); | ||
| stroke-width: calc(var(--swc-color-loupe-outer-border-width, token("color-loupe-outer-border-width")) + 2px); | ||
| } | ||
|
|
||
| /* Compensates for sub-pixel rounding in RTL that shifts the loupe */ | ||
| :host(:dir(rtl)) { | ||
| inset-inline-end: calc(50% - (token("color-loupe-width") / 2) - 1px); | ||
| } | ||
|
|
||
| :host([open]) { | ||
| --swc-color-loupe-opacity: 1; | ||
| --swc-color-loupe-transform: translate(0, 0); | ||
| } | ||
|
|
||
| @media (forced-colors: active) { | ||
|
blunteshwar marked this conversation as resolved.
|
||
| .swc-ColorLoupe-outerBorder { | ||
| --swc-color-loupe-outer-border-color: CanvasText; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| /** | ||
| * Copyright 2026 Adobe. All rights reserved. | ||
| * This file is licensed to you under the Apache License, Version 2.0 (the "License"); | ||
| * you may not use this file except in compliance with the License. You may obtain a copy | ||
| * of the License at http://www.apache.org/licenses/LICENSE-2.0 | ||
| * | ||
| * Unless required by applicable law or agreed to in writing, software distributed under | ||
| * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS | ||
| * OF ANY KIND, either express or implied. See the License for the specific language | ||
| * governing permissions and limitations under the License. | ||
| */ | ||
| import { defineElement } from '@spectrum-web-components/core/element/index.js'; | ||
|
|
||
| import { ColorLoupe } from './ColorLoupe.js'; | ||
|
|
||
| export * from './ColorLoupe.js'; | ||
| declare global { | ||
| interface HTMLElementTagNameMap { | ||
| 'swc-color-loupe': ColorLoupe; | ||
| } | ||
| } | ||
| defineElement('swc-color-loupe', ColorLoupe); |
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.
We should be validating color is a valid CSS string and warn/error if its invalid. I could see a method similar to
ifDefinedthat we can use across other components expecting CSS strings.isValidColororisCSSColorsomething along those lines. could be established here or just go straight to core tools as a utility method.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.
We might want to leave that to implementation to use their own validation to avoid any perf load and also they might accept a format that we don't include. Possibly a TODO to investigate later?
Uh oh!
There was an error while loading. Please reload this page.
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.
when i say validating i mean string validation that it matches a color value structure. like does it match an rgba(0, 0, 0, 0) as a string for an example.
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.
We already have a method
public validateColorStringinColorController.tsin reactive controllers. Since color-loupe always receives color from parent-component hence it does not usevalidateColorStringmethod.Uh oh!
There was an error while loading. Please reload this page.
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.
ColorController.validateColorStringis the right hook if this ever needs standalone validation. Updated the@todoto reference the existing utility rather than proposing a new one, and made the reasoning explicit (the loupe delegates to its parent, which validates upstream):