-
Notifications
You must be signed in to change notification settings - Fork 249
feat: migrate color-loupe to gen2 #6184
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 10 commits
d26745a
7d639ae
88e37da
0592beb
376b110
4c903a0
7001746
e00da6d
f803e26
bfc5c25
9168043
c41ba90
773d452
ee4da78
aa30350
0b2f574
a66db23
c6ae92c
9ca11c6
bc302df
da87509
db9768e
67424c3
f2ff302
5c2c3d3
c6fffb8
9288971
084efa9
1a6290d
cd7cf8a
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,55 @@ | ||
| /** | ||
| * 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'; | ||
|
|
||
| /** | ||
| * 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). | ||
| * | ||
| * Default is semi-transparent red so the opacity checkerboard is visible | ||
| * when the component is rendered without a `color` attribute. | ||
| * | ||
| * @todo Runtime validation is intentionally not performed here. The loupe | ||
| * always receives its color from a parent color-picker component, which | ||
| * validates upstream via `validateColorString` on `ColorController`. If | ||
| * the loupe ever needs standalone validation (e.g. consumed outside a | ||
| * parent color picker), reuse `ColorController.validateColorString` | ||
| * rather than adding a separate utility. | ||
| */ | ||
| @property({ type: String }) | ||
| public color = 'rgba(255, 0, 0, 0.5)'; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| /** | ||
| * 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'; |
| Original file line number | Diff line number | Diff line change | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,90 @@ | ||||||||||||
| /** | ||||||||||||
| * 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 { styleMap } from 'lit/directives/style-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 | ||||||||||||
|
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 believe this should be changed from "preview" to "unsupported"
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! |
||||||||||||
| * @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 SWC-2029 - Migrate opacity-checkerboard to 2nd gen and consume it | ||||||||||||
| * here; checkerboard styling is currently hardcoded in color-loupe.css. | ||||||||||||
| */ | ||||||||||||
| public static override get styles(): CSSResultArray { | ||||||||||||
| return [styles]; | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| protected override render(): TemplateResult { | ||||||||||||
| return html` | ||||||||||||
| <div | ||||||||||||
| class=${classMap({ | ||||||||||||
| ['swc-ColorLoupe']: true, | ||||||||||||
| })} | ||||||||||||
|
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.
Suggested change
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. Thanks @rubencarvalho — you’re right that a plain class="swc-ColorLoupe" would be enough for a single, unconditional class. I’m going to keep classMap on the root wrapper anyway so this line matches the same pattern we use in other 2nd-gen components (static single-class classMap on the top-level block) and stays consistent if we add conditional classes later (e.g. state/variant) without reworking the template. If we decide as a team to standardize on plain class for the non-conditional case, I’m happy to follow this up in a small cleanup PR.
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 disagree here. This should only be flattened to a class as @rubencarvalho suggested. Though this is not a merge blocker for now.
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 still think this is overcomplicating it 😅 it's 1 string so we don't need the classMap here, looking at the other components it seems like we're not doing it there either. cc: @miwha-adobe |
||||||||||||
| > | ||||||||||||
| <div class="swc-ColorLoupe-checkerboard swc-ColorLoupe--clipped"></div> | ||||||||||||
| <div | ||||||||||||
| class="swc-ColorLoupe-colorFill swc-ColorLoupe--clipped" | ||||||||||||
| style=${styleMap({ | ||||||||||||
| '--swc-color-loupe-picked-color': this.color, | ||||||||||||
| })} | ||||||||||||
| ></div> | ||||||||||||
| <svg aria-hidden="true" class="swc-ColorLoupe-svg" overflow="visible"> | ||||||||||||
| <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,116 @@ | ||
| /** | ||
| * 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. | ||
| */ | ||
|
|
||
| :host { | ||
| --_swc-color-loupe-width: token("color-loupe-width"); | ||
|
|
||
| display: block; | ||
| position: absolute; | ||
| inset-block-end: calc((token("color-handle-size") - token("color-handle-outer-border-width")) + token("color-loupe-bottom-to-color-handle")); | ||
| inset-inline-end: calc(50% - (var(--_swc-color-loupe-width) / 2)); | ||
| inline-size: var(--_swc-color-loupe-width); | ||
| block-size: token("color-loupe-height"); | ||
| } | ||
|
|
||
| * { | ||
| box-sizing: border-box; | ||
| } | ||
|
|
||
| .swc-ColorLoupe { | ||
| position: relative; | ||
| inline-size: 100%; | ||
| block-size: 100%; | ||
| pointer-events: none; | ||
| opacity: 0; | ||
| filter: drop-shadow(token("drop-shadow-elevated-x") token("drop-shadow-elevated-y") token("drop-shadow-elevated-blur") token("drop-shadow-elevated-color")); | ||
|
|
||
| /* TODO: replace 8px with a forthcoming animation-distance token (matches Spectrum CSS S2). */ | ||
|
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. Same with this todo. Do we have a ticket #, or is it a necessary todo? When will this animation token be available?
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. The 8px value here corresponds to --mod-colorloupe-animation-distance in Spectrum CSS S2 (colorloupe/index.css), which controls how far the loupe translates down when it transitions to the closed state. That mod-property was intentionally not carried over as a consumer-facing hook (see the CSS custom properties section), but the underlying value also doesn't map to any token in the 2nd-gen token catalog yet. I've left the TODO so we can swap it to a token("color-loupe-animation-distance") call once it lands in spectrum tokens, rather than silently burying a magic number.
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. @5t3ph do you happen to have insight into when the s2 token will be carried over?
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. You can remove the TODO, looking at the Spectrum CSS source, it seems this is in fact a magic number, and there's not a token planned. Looking at Color handle, the default gap value is
If we find we need to modify it from the consuming components, we could expose a custom property to override the 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. What would you recommend here? Should I remove the TODO and keep the hardcoded 8px value, or expose it as a custom property instead?
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. Let's remove the TODO and keep the hardcoded value for now
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! |
||
| transform: translateY(8px); | ||
| 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 set by the component via | ||
| the --swc-color-loupe-picked-color custom property. */ | ||
| .swc-ColorLoupe-colorFill { | ||
| position: absolute; | ||
| inset-block-start: 2px; | ||
| inset-inline-start: 2px; | ||
| inline-size: 100%; | ||
| block-size: 100%; | ||
| background: var(--swc-color-loupe-picked-color); | ||
| } | ||
|
|
||
| /* SVG overlay — inherits host dimensions */ | ||
| .swc-ColorLoupe-svg { | ||
| position: absolute; | ||
| inline-size: inherit; | ||
| block-size: inherit; | ||
| } | ||
|
|
||
| /* Inner border: thin stroke separating color fill from the loupe edge */ | ||
| .swc-ColorLoupe-innerBorder { | ||
| fill: none; | ||
| stroke: token("color-loupe-inner-border"); | ||
| stroke-width: token("color-loupe-inner-border-width"); | ||
| } | ||
|
|
||
| /* Outer border: wider stroke forming the loupe outline */ | ||
| .swc-ColorLoupe-outerBorder { | ||
| --_swc-color-loupe-outer-border-color: token("color-loupe-outer-border"); | ||
|
|
||
| fill: none; | ||
| stroke: var(--_swc-color-loupe-outer-border-color); | ||
| stroke-width: calc(token("color-loupe-outer-border-width") + 2px); | ||
| } | ||
|
|
||
| /* Clip utility — applied to the checkerboard and color-fill layers to | ||
| constrain them to the loupe teardrop silhouette. Modifier class is placed | ||
| after all subcomponent rules per the documented rule order. */ | ||
| .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"); | ||
| } | ||
|
|
||
| /* Compensates for sub-pixel rounding in RTL that shifts the loupe */ | ||
| :host(:dir(rtl)) { | ||
| inset-inline-end: calc(50% - (var(--_swc-color-loupe-width) / 2) - 1px); | ||
| } | ||
|
|
||
| :host([open]) .swc-ColorLoupe { | ||
| opacity: 1; | ||
| transform: translate(0, 0); | ||
| } | ||
|
|
||
| @media (forced-colors: active) { | ||
| .swc-ColorLoupe-colorFill, | ||
| .swc-ColorLoupe-checkerboard { | ||
| forced-color-adjust: none; | ||
| } | ||
|
|
||
| .swc-ColorLoupe-outerBorder { | ||
| --_swc-color-loupe-outer-border-color: CanvasText; | ||
| } | ||
| } | ||

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.
Can we include a ticket number here. What ticket is responsible for making sure this todo is completed?
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.
Good call — I've removed the @todo. Since swc-color-loupe always receives its color value from a parent color component (e.g. swc-color-area, swc-color-slider) that already validates the color string upstream via ColorController, there's no need to add a second validation layer here or track it as future work.