@erclx/canon 5.53.0 → 5.54.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +10 -9
  3. package/claude/.claude-plugin/plugin.json +1 -1
  4. package/claude/skills/canon-cli/SKILL.md +5 -5
  5. package/claude/skills/design-identity/REQUIREMENT.md +2 -2
  6. package/claude/skills/design-identity/SKILL.md +6 -6
  7. package/claude/skills/design-record/REQUIREMENT.md +2 -2
  8. package/claude/skills/design-record/SKILL.md +4 -4
  9. package/claude/skills/design-record/references/folder.md +1 -1
  10. package/claude/skills/design-taste/REQUIREMENT.md +1 -1
  11. package/claude/skills/draw-figure/REQUIREMENT.md +1 -1
  12. package/claude/skills/draw-figure/SKILL.md +1 -1
  13. package/claude/skills/pick-by-eye/REQUIREMENT.md +4 -4
  14. package/claude/skills/pick-by-eye/SKILL.md +34 -41
  15. package/claude/skills/pick-by-eye/references/board.md +42 -0
  16. package/claude/skills/pick-by-eye/references/live-arms.md +3 -3
  17. package/claude/skills/plan-groundwork/SKILL.md +1 -1
  18. package/claude/skills/teach-craft/REQUIREMENT.md +1 -1
  19. package/claude/skills/teach-craft/SKILL.md +32 -1
  20. package/claude/skills/teach-craft/references/adopted.md +113 -0
  21. package/claude/skills/teach-craft/references/lesson-craft.md +10 -2
  22. package/claude/skills/teach-craft/references/pedagogy.md +29 -2
  23. package/docs/agents/install-and-sync.md +0 -8
  24. package/docs/target/migrations.md +8 -3
  25. package/docs/target/projects.md +1 -1
  26. package/docs/workflow/skill-map.md +0 -1
  27. package/docs/workflow/visual-design-workflow.md +1 -4
  28. package/governance/detect.toml +1 -6
  29. package/governance/renames.toml +0 -1
  30. package/package.json +2 -5
  31. package/src/claude/cases/misc.ts +0 -5
  32. package/src/claude/skills-audit.ts +1 -0
  33. package/src/cli/help.ts +0 -3
  34. package/src/cli.ts +0 -2
  35. package/src/commands/design.ts +16 -315
  36. package/src/commands/sync.ts +4 -5
  37. package/src/design/regen.ts +5 -32
  38. package/src/design/render.ts +1 -1
  39. package/src/design/tokens.ts +3 -2
  40. package/src/gate/stages.ts +4 -10
  41. package/src/gov/audit.ts +1 -6
  42. package/src/markdown/scope.ts +2 -4
  43. package/src/shipped/references.ts +1 -2
  44. package/src/sync/check.ts +4 -29
  45. package/src/sync/stamp.ts +6 -6
  46. package/src/sync/target.ts +1 -4
  47. package/tooling/claude/reference.md +1 -1
  48. package/tooling/claude/seeds/canon/DESIGN.md +1 -1
  49. package/claude/skills/canvas-board/REQUIREMENT.md +0 -35
  50. package/claude/skills/canvas-board/SKILL.md +0 -143
  51. package/claude/skills/canvas-board/references/canvas.md +0 -170
  52. package/governance/rules/ui/canvas.md +0 -13
  53. package/src/canvas/address.ts +0 -227
  54. package/src/canvas/arrange.ts +0 -66
  55. package/src/canvas/capture.ts +0 -391
  56. package/src/canvas/client/app.tsx +0 -188
  57. package/src/canvas/client/inspector/align-grid.tsx +0 -162
  58. package/src/canvas/client/inspector/color-field.tsx +0 -398
  59. package/src/canvas/client/inspector/color-inputs.tsx +0 -139
  60. package/src/canvas/client/inspector/color-picker.tsx +0 -668
  61. package/src/canvas/client/inspector/color-space.ts +0 -240
  62. package/src/canvas/client/inspector/field.tsx +0 -263
  63. package/src/canvas/client/inspector/fields.css +0 -763
  64. package/src/canvas/client/inspector/section.tsx +0 -27
  65. package/src/canvas/client/inspector/size-field.tsx +0 -69
  66. package/src/canvas/client/inspector/token-select.tsx +0 -109
  67. package/src/canvas/client/inspector/values.ts +0 -297
  68. package/src/canvas/client/inspector.tsx +0 -692
  69. package/src/canvas/client/keys.ts +0 -71
  70. package/src/canvas/client/layers.tsx +0 -118
  71. package/src/canvas/client/main.tsx +0 -71
  72. package/src/canvas/client/pages-panel.tsx +0 -212
  73. package/src/canvas/client/panel-handle.tsx +0 -90
  74. package/src/canvas/client/selection.tsx +0 -199
  75. package/src/canvas/client/shell.css +0 -762
  76. package/src/canvas/client/state.ts +0 -792
  77. package/src/canvas/client/surface.tsx +0 -803
  78. package/src/canvas/client/theme-panel.tsx +0 -55
  79. package/src/canvas/client/tools.tsx +0 -153
  80. package/src/canvas/content.ts +0 -781
  81. package/src/canvas/edit.ts +0 -575
  82. package/src/canvas/editing.ts +0 -203
  83. package/src/canvas/fit.ts +0 -238
  84. package/src/canvas/global.d.ts +0 -15
  85. package/src/canvas/history.ts +0 -116
  86. package/src/canvas/index.html +0 -20
  87. package/src/canvas/server.ts +0 -766
  88. package/src/canvas/tokens.ts +0 -221
  89. package/src/commands/canvas.ts +0 -1097
  90. package/src/design/adapter.ts +0 -59
  91. package/src/design/base.css +0 -203
  92. package/src/design/brand-favicon.ts +0 -18
  93. package/src/design/neutral.ts +0 -127
  94. package/src/design/project-fonts.ts +0 -392
@@ -1,27 +0,0 @@
1
- /** @jsxImportSource preact */
2
- import { type ComponentChildren, type JSX, toChildArray } from 'preact'
3
-
4
- interface SectionProps {
5
- readonly title: string
6
- /** Sits in a fixed lane right of the title, so adding one never moves it. */
7
- readonly action?: ComponentChildren
8
- readonly children?: ComponentChildren
9
- }
10
-
11
- /** A titled group of fields. With no fields it renders its header alone. */
12
- export function Section({
13
- title,
14
- action,
15
- children,
16
- }: SectionProps): JSX.Element {
17
- const fields = toChildArray(children)
18
- return (
19
- <section class="inspector-section">
20
- <header class="inspector-section-head">
21
- <h3 class="inspector-section-title">{title}</h3>
22
- <span class="inspector-section-action">{action}</span>
23
- </header>
24
- {fields.length > 0 ? <div class="field-grid">{fields}</div> : null}
25
- </section>
26
- )
27
- }
@@ -1,69 +0,0 @@
1
- /** @jsxImportSource preact */
2
- import type { JSX } from 'preact'
3
- import { Field, type Scrub } from '@/canvas/client/inspector/field'
4
-
5
- /** The modes a size takes besides a number, by the name the field shows. */
6
- const MODES = ['Fill', 'Fit'] as const
7
-
8
- interface SizeFieldProps {
9
- readonly label: string
10
- readonly glyph: string
11
- /** The value as the panel shows it, a mode name or a number. */
12
- readonly initial: string
13
- readonly isBusy: boolean
14
- /** Takes a typed number or a picked mode name, each as shown. */
15
- readonly onCommit: (value: string) => void
16
- readonly scrub?: Scrub
17
- }
18
-
19
- /**
20
- * A size that takes a number or a mode. The chevron is a native select, so
21
- * its list opens above the panel's clipping and answers the keyboard as the
22
- * platform does, and typing a number replaces the mode.
23
- */
24
- export function SizeField({
25
- label,
26
- glyph,
27
- initial,
28
- isBusy,
29
- onCommit,
30
- scrub,
31
- }: SizeFieldProps): JSX.Element {
32
- const mode = (MODES as readonly string[]).includes(initial) ? initial : ''
33
- return (
34
- <Field
35
- label={label}
36
- glyph={glyph}
37
- initial={initial}
38
- isBusy={isBusy}
39
- onCommit={onCommit}
40
- scrub={scrub}
41
- trailing={
42
- <span class="size-mode">
43
- <select
44
- class="size-mode-select"
45
- aria-label={`${label} mode`}
46
- value={mode}
47
- disabled={isBusy}
48
- onChange={(event) => {
49
- const picked = event.currentTarget.value
50
- if (picked && picked !== mode) onCommit(picked)
51
- }}
52
- >
53
- <option value="" disabled hidden>
54
- Fixed
55
- </option>
56
- {MODES.map((name) => (
57
- <option key={name} value={name}>
58
- {name}
59
- </option>
60
- ))}
61
- </select>
62
- <svg class="size-mode-chevron" viewBox="0 0 16 16" aria-hidden="true">
63
- <path d="M4.5 6.5 8 10l3.5-3.5" />
64
- </svg>
65
- </span>
66
- }
67
- />
68
- )
69
- }
@@ -1,109 +0,0 @@
1
- /** @jsxImportSource preact */
2
- import type { JSX } from 'preact'
3
- import { Field, type Scrub } from '@/canvas/client/inspector/field'
4
-
5
- /** The menu entry that hands the value back to the field for typing. */
6
- const RAW = ''
7
-
8
- interface TokenSelectProps {
9
- readonly label: string
10
- readonly glyph: string
11
- /** The value as the panel shows it, a token's name where one stands. */
12
- readonly initial: string
13
- /** One token group's names, in the order the token sheet sorts them. */
14
- readonly tokens: readonly string[]
15
- /** The token the element names, where it names one. */
16
- readonly token?: string
17
- readonly isRaw: boolean
18
- readonly isBusy: boolean
19
- readonly isWide?: boolean
20
- readonly placeholder?: string
21
- /** Takes a typed value as typed. */
22
- readonly onCommit: (value: string) => void
23
- /** Takes the name of a token picked from the menu or typed whole. */
24
- readonly onPick: (name: string) => void
25
- readonly scrub?: Scrub
26
- }
27
-
28
- /**
29
- * A value that takes a token or a typed literal. The menu lists the group's
30
- * tokens first and a raw value last, which returns focus to the field, so a
31
- * token is one pick away and a literal is still one keystroke. A group with no
32
- * tokens leaves the field alone, with no menu and no raw marker.
33
- */
34
- export function TokenSelect({
35
- label,
36
- glyph,
37
- initial,
38
- tokens,
39
- token,
40
- isRaw,
41
- isBusy,
42
- isWide,
43
- placeholder,
44
- onCommit,
45
- onPick,
46
- scrub,
47
- }: TokenSelectProps): JSX.Element {
48
- const hasTokens = tokens.length > 0
49
- return (
50
- <Field
51
- label={label}
52
- glyph={glyph}
53
- initial={initial}
54
- isBusy={isBusy}
55
- isWide={isWide}
56
- placeholder={placeholder}
57
- onCommit={(typed) => {
58
- if (tokens.includes(typed)) onPick(typed)
59
- else onCommit(typed)
60
- }}
61
- scrub={scrub}
62
- trailing={
63
- hasTokens ? (
64
- <>
65
- {isRaw ? (
66
- <span
67
- class="raw"
68
- title="Set as a raw value, so it will not follow the theme. Pick a token to fix it"
69
- >
70
- raw
71
- </span>
72
- ) : null}
73
- <span class="size-mode">
74
- <select
75
- class="size-mode-select"
76
- aria-label={`${label} tokens`}
77
- value={token ?? RAW}
78
- disabled={isBusy}
79
- onChange={(event) => {
80
- const picked = event.currentTarget.value
81
- if (picked === RAW) {
82
- event.currentTarget
83
- .closest('.glyph-field')
84
- ?.querySelector<HTMLInputElement>('.glyph-field-input')
85
- ?.focus()
86
- } else if (picked !== token) onPick(picked)
87
- }}
88
- >
89
- {tokens.map((name) => (
90
- <option key={name} value={name}>
91
- {name}
92
- </option>
93
- ))}
94
- <option value={RAW}>Raw value</option>
95
- </select>
96
- <svg
97
- class="size-mode-chevron"
98
- viewBox="0 0 16 16"
99
- aria-hidden="true"
100
- >
101
- <path d="M4.5 6.5 8 10l3.5-3.5" />
102
- </svg>
103
- </span>
104
- </>
105
- ) : null
106
- }
107
- />
108
- )
109
- }
@@ -1,297 +0,0 @@
1
- /**
2
- * How the inspector reads a computed value and writes a typed one back. A
3
- * field shows the formatted form, and only a value the operator changed goes
4
- * back through `toCssValue`, so rounding never writes a value nobody typed.
5
- */
6
-
7
- /** A length that runs below zero, so a bare number is pixels and unclamped. */
8
- const SIGNED_LENGTHS = new Set(['letter-spacing'])
9
-
10
- /** Properties whose bare numbers are pixel lengths. */
11
- const LENGTHS = new Set([
12
- 'width',
13
- 'height',
14
- 'padding',
15
- 'gap',
16
- 'font-size',
17
- 'border-radius',
18
- 'border-top-left-radius',
19
- 'border-top-right-radius',
20
- 'border-bottom-right-radius',
21
- 'border-bottom-left-radius',
22
- ])
23
-
24
- /** Properties a size mode writes, each mode by the name the field shows. */
25
- const SIZES = new Set(['width', 'height'])
26
- const SIZE_MODES: Readonly<Record<string, string>> = {
27
- Fill: '100%',
28
- Fit: 'fit-content',
29
- }
30
-
31
- const PX = /^(-?\d*\.?\d+)px$/
32
- const EM = /^(-?\d*\.?\d+)em$/
33
- const NUMBER = /^-?\d*\.?\d+$/
34
-
35
- function formatPx(token: string): string {
36
- const match = token.match(PX)
37
- if (!match) return token
38
- const value = Number(match[1])
39
- return String(
40
- Math.abs(value) < 10 ? Math.round(value * 10) / 10 : Math.round(value),
41
- )
42
- }
43
-
44
- /** A color that paints nothing, which the panel reads as no fill. */
45
- export function isNoFill(value: string): boolean {
46
- const normalized = value.trim().toLowerCase().replace(/\s+/g, '')
47
- return normalized === 'transparent' || normalized === 'rgba(0,0,0,0)'
48
- }
49
-
50
- export function displayValue(property: string, raw: string): string {
51
- const value = raw.trim()
52
- if (property === 'gap' && (value === '' || value === 'normal')) return ''
53
- if (property === 'letter-spacing') {
54
- if (value === 'normal') return ''
55
- const em = value.match(EM)
56
- if (em) return `${Math.round(Number(em[1]) * 1000) / 1000}em`
57
- }
58
- if (property === 'background-color' && isNoFill(value)) return ''
59
- if (property === 'opacity' && value !== '')
60
- return `${Math.round(Number(value) * 100)}%`
61
- if (SIZES.has(property)) {
62
- const mode = Object.keys(SIZE_MODES).find(
63
- (name) => SIZE_MODES[name] === value,
64
- )
65
- if (mode) return mode
66
- }
67
- if (value === 'auto') return 'Auto'
68
- if (value === 'normal') return 'Normal'
69
- return value.split(/\s+/).map(formatPx).join(' ')
70
- }
71
-
72
- export function toCssValue(property: string, typed: string): string {
73
- const value = typed.trim()
74
- /* A line height has no auto keyword, so the field's Auto is its normal. */
75
- if (property === 'line-height' && value.toLowerCase() === 'auto') {
76
- return 'normal'
77
- }
78
- if (value === 'Auto' || value === 'Normal') return value.toLowerCase()
79
- if (SIZES.has(property) && SIZE_MODES[value]) return SIZE_MODES[value]
80
- if (!LENGTHS.has(property) && !SIGNED_LENGTHS.has(property)) return value
81
- return value
82
- .split(/\s+/)
83
- .map((token) => (/^-?\d*\.?\d+$/.test(token) ? `${token}px` : token))
84
- .join(' ')
85
- }
86
-
87
- /** A typed opacity percent as the fraction CSS takes, refused outside 0 to 100. */
88
- export function opacityToCss(typed: string): string | undefined {
89
- const percent = readOpacity(typed)
90
- return percent === undefined ? undefined : String(percent / 100)
91
- }
92
-
93
- /**
94
- * The radius all four corners share, read in the order the longhands run,
95
- * or undefined where any corner differs and the panel shows each one.
96
- */
97
- export function sharedRadius(corners: readonly string[]): string | undefined {
98
- const [first] = corners
99
- return corners.every((corner) => corner === first) ? first : undefined
100
- }
101
-
102
- /**
103
- * Holds a scrubbed number inside what the property accepts. The writer takes
104
- * any declaration that does not break the rule, so a negative length or a
105
- * weight past 1000 would reach the file and the browser would drop it there.
106
- */
107
- export function clampScrub(property: string, value: number): number {
108
- if (property === 'font-weight') return Math.min(1000, Math.max(1, value))
109
- if (LENGTHS.has(property) || property === 'line-height') {
110
- return Math.max(0, value)
111
- }
112
- return value
113
- }
114
-
115
- /**
116
- * A line height as the panel shows it: a ratio of the font size, the way a
117
- * unitless value writes it. A browser computes a unitless value to pixels, so
118
- * a pixel value reads back as its ratio when the font size is in pixels too.
119
- */
120
- export function lineHeightValue(raw: string, fontSize: string): string {
121
- const value = raw.trim()
122
- if (value === 'normal') return 'Auto'
123
- const ratio = (number: number): string =>
124
- String(Math.round(number * 100) / 100)
125
- if (NUMBER.test(value)) return ratio(Number(value))
126
- const height = value.match(PX)
127
- const size = fontSize.trim().match(PX)
128
- if (height && size && Number(size[1]) > 0) {
129
- return ratio(Number(height[1]) / Number(size[1]))
130
- }
131
- return value
132
- }
133
-
134
- /** The first family of a stack, unquoted, which is the one the field names. */
135
- export function firstFamily(stack: string): string {
136
- const value = stack.trim()
137
- if (value.startsWith('var(')) return value
138
- const [first = ''] = value.split(',')
139
- return first.trim().replace(/^(["'])(.*)\1$/, '$2')
140
- }
141
-
142
- /**
143
- * Text alignment by the side it lands on, so the group presses the side the
144
- * text paints on. The logical keywords turn on the element's direction.
145
- */
146
- export function textAlignOf(computed: string, direction: string): string {
147
- const value = computed.trim()
148
- const isRtl = direction.trim() === 'rtl'
149
- if (value === 'start') return isRtl ? 'right' : 'left'
150
- if (value === 'end') return isRtl ? 'left' : 'right'
151
- return value
152
- }
153
-
154
- /**
155
- * A value as the writer takes it. A whole declaration pasted from a
156
- * stylesheet keeps its value alone, since the writer refuses the semicolon.
157
- */
158
- export function declarationValue(property: string, typed: string): string {
159
- const value = typed.trim().replace(/;\s*$/, '')
160
- const prefix = `${property}:`
161
- return value.toLowerCase().startsWith(prefix)
162
- ? value.slice(prefix.length).trim()
163
- : value
164
- }
165
-
166
- /** The token a value names when it is a bare `var()` and nothing else. */
167
- export function tokenOf(value: string): string | undefined {
168
- return value.trim().match(VAR)?.[1]
169
- }
170
-
171
- /** Units a scrub moves for each pixel the pointer travels. */
172
- export function scrubStep(isCoarse: boolean): number {
173
- return isCoarse ? 10 : 1
174
- }
175
-
176
- /** A color the picker can write: a token or a hex, each with an opacity. */
177
- export type WrittenColor =
178
- | {
179
- readonly kind: 'token'
180
- /** The custom property name as the token sheet gives it. */
181
- readonly name: string
182
- readonly opacity: number
183
- }
184
- | { readonly kind: 'hex'; readonly hex: string; readonly opacity: number }
185
-
186
- export type ParsedColor = WrittenColor | { readonly kind: 'none' }
187
-
188
- const HEX = /^#?([0-9a-f]{3}|[0-9a-f]{4}|[0-9a-f]{6}|[0-9a-f]{8})$/i
189
- const RGB =
190
- /^rgba?\(\s*([\d.]+)[\s,]+([\d.]+)[\s,]+([\d.]+)(?:\s*[,/]\s*([\d.]+%?))?\s*\)$/i
191
- /** What a browser computes `color-mix()` in srgb to, channels 0 to 1. */
192
- const SRGB =
193
- /^color\(\s*srgb\s+([\d.]+)\s+([\d.]+)\s+([\d.]+)(?:\s*\/\s*([\d.]+%?))?\s*\)$/i
194
- const VAR = /^var\(\s*(--[\w-]+)\s*\)$/i
195
- const MIX =
196
- /^color-mix\(\s*in\s+srgb\s*,\s*var\(\s*(--[\w-]+)\s*\)\s+([\d.]+)%\s*,\s*transparent\s*\)$/i
197
-
198
- function channel(value: number): string {
199
- return Math.round(Math.min(255, Math.max(0, value)))
200
- .toString(16)
201
- .padStart(2, '0')
202
- }
203
-
204
- /**
205
- * An alpha as a percent. A value past 1 clamps to full rather than reading as
206
- * a 0 to 255 scale, since CSS clamps it the same way when it paints.
207
- */
208
- function alphaPercent(alpha: string | undefined): number {
209
- if (alpha === undefined) return 100
210
- const fraction = alpha.endsWith('%')
211
- ? Number(alpha.slice(0, -1)) / 100
212
- : Number(alpha)
213
- return Math.round(Math.min(1, Math.max(0, fraction)) * 100)
214
- }
215
-
216
- /** A typed hex in any of its lengths, normalized to six digits. */
217
- export function readHex(
218
- typed: string,
219
- ): { readonly hex: string; readonly opacity?: number } | undefined {
220
- const match = typed.trim().match(HEX)
221
- if (!match) return undefined
222
- const digits = match[1].toLowerCase()
223
- const full =
224
- digits.length <= 4
225
- ? [...digits].map((digit) => digit + digit).join('')
226
- : digits
227
- const hex = full.slice(0, 6)
228
- if (full.length === 6) return { hex }
229
- return {
230
- hex,
231
- opacity: Math.round((parseInt(full.slice(6), 16) / 255) * 100),
232
- }
233
- }
234
-
235
- /** A typed opacity percent, refused outside 0 to 100. */
236
- export function readOpacity(typed: string): number | undefined {
237
- const value = typed.trim().replace(/%$/, '')
238
- if (!/^-?\d*\.?\d+$/.test(value)) return undefined
239
- const percent = Number(value)
240
- return percent >= 0 && percent <= 100 ? Math.round(percent) : undefined
241
- }
242
-
243
- /**
244
- * Reads a computed or inline color into what the picker shows. A form it does
245
- * not know, such as a named color, comes back undefined.
246
- */
247
- export function parseColor(value: string): ParsedColor | undefined {
248
- const text = value.trim()
249
- if (isNoFill(text)) return { kind: 'none' }
250
- const token = text.match(VAR)
251
- if (token) return { kind: 'token', name: token[1], opacity: 100 }
252
- const mix = text.match(MIX)
253
- if (mix) {
254
- return {
255
- kind: 'token',
256
- name: mix[1],
257
- opacity: Math.round(Math.min(100, Number(mix[2]))),
258
- }
259
- }
260
- const hex = text.startsWith('#') ? readHex(text) : undefined
261
- if (hex) return painted(hex.hex, hex.opacity ?? 100)
262
- const rgb = text.match(RGB)
263
- if (rgb) {
264
- return painted(
265
- [rgb[1], rgb[2], rgb[3]].map((part) => channel(Number(part))).join(''),
266
- alphaPercent(rgb[4]),
267
- )
268
- }
269
- const srgb = text.match(SRGB)
270
- if (!srgb) return undefined
271
- return painted(
272
- [srgb[1], srgb[2], srgb[3]]
273
- .map((part) => channel(Number(part) * 255))
274
- .join(''),
275
- alphaPercent(srgb[4]),
276
- )
277
- }
278
-
279
- /** A color at zero opacity paints nothing, whichever form wrote it. */
280
- function painted(hex: string, opacity: number): ParsedColor {
281
- return opacity === 0 ? { kind: 'none' } : { kind: 'hex', hex, opacity }
282
- }
283
-
284
- /**
285
- * The value a pick writes. A token below full opacity mixes with transparent
286
- * rather than resolving to a hex, so it keeps following the theme.
287
- */
288
- export function composeColor(color: WrittenColor): string {
289
- if (color.kind === 'token') {
290
- return color.opacity >= 100
291
- ? `var(${color.name})`
292
- : `color-mix(in srgb, var(${color.name}) ${color.opacity}%, transparent)`
293
- }
294
- return color.opacity >= 100
295
- ? `#${color.hex}`
296
- : `#${color.hex}${channel((color.opacity / 100) * 255)}`
297
- }