@nexxtmove/ui 2.2.0 → 2.4.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.
- package/dist/components/editor/molecules/EditorBackgroundSize/EditorBackgroundSize.vue.d.ts +2 -2
- package/dist/components/editor/molecules/EditorBlur/EditorBlur.vue.d.ts +2 -2
- package/dist/components/editor/molecules/EditorColor/EditorColor.vue.d.ts +44 -20
- package/dist/components/editor/molecules/EditorColor/_ColorField.vue.d.ts +66 -0
- package/dist/components/editor/molecules/EditorColor/colorClass.d.ts +46 -0
- package/dist/components/editor/molecules/EditorColor/gradientClass.d.ts +80 -0
- package/dist/components/editor/molecules/EditorGap/EditorGap.vue.d.ts +2 -2
- package/dist/components/editor/molecules/EditorMaxWidth/EditorMaxWidth.vue.d.ts +2 -2
- package/dist/components/editor/molecules/EditorShadow/EditorShadow.vue.d.ts +2 -2
- package/dist/components/organisms/ColorPicker/ColorPicker.vue.d.ts +58 -2
- package/dist/components/organisms/ColorPicker/_GradientField.vue.d.ts +23 -0
- package/dist/components/organisms/ColorPicker/_GradientPreview.vue.d.ts +11 -0
- package/dist/components/organisms/ColorPicker/_GradientStopRow.vue.d.ts +21 -0
- package/dist/components/organisms/ColorPicker/_GradientStops.vue.d.ts +19 -0
- package/dist/components/organisms/ColorPicker/gradient.d.ts +189 -0
- package/dist/components/organisms/ColorPicker/gradientGeometry.d.ts +101 -0
- package/dist/components/organisms/ColorPicker/useGradientEditor.d.ts +89 -0
- package/dist/components/organisms/VideoControls/VideoControls.vue.d.ts +8 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.js +3400 -2083
- package/package.json +1 -1
- package/src/components/editor/molecules/EditorColor/EditorColor.vue +83 -171
- package/src/components/editor/molecules/EditorColor/_ColorField.vue +445 -0
- package/src/components/editor/molecules/EditorColor/colorClass.ts +61 -0
- package/src/components/editor/molecules/EditorColor/gradientClass.ts +225 -0
- package/src/components/molecules/ToggleGroup/ToggleGroup.vue +6 -2
- package/src/components/organisms/ColorPicker/ColorPicker.vue +673 -271
- package/src/components/organisms/ColorPicker/_GradientField.vue +93 -0
- package/src/components/organisms/ColorPicker/_GradientPreview.vue +401 -0
- package/src/components/organisms/ColorPicker/_GradientStopRow.vue +181 -0
- package/src/components/organisms/ColorPicker/_GradientStops.vue +218 -0
- package/src/components/organisms/ColorPicker/gradient.ts +303 -0
- package/src/components/organisms/ColorPicker/gradientGeometry.ts +216 -0
- package/src/components/organisms/ColorPicker/useGradientEditor.ts +358 -0
- package/src/components/organisms/VideoControls/VideoControls.vue +15 -4
- package/src/index.ts +25 -0
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The class contract of a gradient background, as NexxtEditorColor writes it:
|
|
3
|
+
* one string of space-separated Tailwind classes in a fixed order --
|
|
4
|
+
*
|
|
5
|
+
* type → from → from-position → via → via-position → to → to-position
|
|
6
|
+
*
|
|
7
|
+
* such as `'bg-linear-135 from-[#7f56d9] via-theme-main via-40% to-[#432e73cc]'`.
|
|
8
|
+
* Equal gradients therefore give equal strings, so a consumer can deduplicate
|
|
9
|
+
* undo steps. Hexes are written in lower case and an opaque `ff` pair is left
|
|
10
|
+
* out; a position is only written when it differs from Tailwind's default (0 %
|
|
11
|
+
* for `from`, 50 % for `via`, 100 % for `to`). No `var()` and no free
|
|
12
|
+
* `bg-[linear-gradient(…)]` is ever written.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { COLOR_ROLES, isHex } from './colorClass'
|
|
16
|
+
import type { EditorColorRole } from './colorClass'
|
|
17
|
+
import { buildGradientCss } from '../../../organisms/ColorPicker/gradient'
|
|
18
|
+
|
|
19
|
+
/** A linear gradient: `bg-linear-<0–359>`, followed by its stops. */
|
|
20
|
+
export type EditorColorLinearGradientValue = `bg-linear-${number} ${string}`
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* A radial gradient: `bg-radial` when its centre is the middle, or
|
|
24
|
+
* `bg-radial-[at_<x>%_<y>%]`, followed by its stops.
|
|
25
|
+
*/
|
|
26
|
+
export type EditorColorRadialGradientValue = `bg-radial ${string}` | `bg-radial-[at_${string}`
|
|
27
|
+
|
|
28
|
+
/** A gradient background as the class string of the contract above. */
|
|
29
|
+
export type EditorColorGradientValue =
|
|
30
|
+
EditorColorLinearGradientValue | EditorColorRadialGradientValue
|
|
31
|
+
|
|
32
|
+
/** The colour of a stop: a literal hex in normal form, or a theme role (always opaque). */
|
|
33
|
+
export type GradientClassColor = { hex: `#${string}` } | { role: EditorColorRole }
|
|
34
|
+
|
|
35
|
+
/** One stop of a parsed gradient class: its colour and its position. */
|
|
36
|
+
export interface GradientClassStop {
|
|
37
|
+
color: GradientClassColor
|
|
38
|
+
/** Whole percentage, 0–100. */
|
|
39
|
+
position: number
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** The kind of gradient: `bg-linear-*` or `bg-radial`/`bg-radial-[at_…]`. */
|
|
43
|
+
export type GradientClassType = 'linear' | 'radial'
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* A gradient class string, parsed: its type with the angle or the centre, and
|
|
47
|
+
* two or three stops in order of position (from, optional via, to).
|
|
48
|
+
*/
|
|
49
|
+
export type GradientClass =
|
|
50
|
+
| {
|
|
51
|
+
type: 'linear'
|
|
52
|
+
/** Whole degrees, 0–359. */
|
|
53
|
+
angle: number
|
|
54
|
+
stops: GradientClassStop[]
|
|
55
|
+
}
|
|
56
|
+
| {
|
|
57
|
+
type: 'radial'
|
|
58
|
+
/** Centre, in whole percentages from the left (0–100). */
|
|
59
|
+
x: number
|
|
60
|
+
/** Centre, in whole percentages from the top (0–100). */
|
|
61
|
+
y: number
|
|
62
|
+
stops: GradientClassStop[]
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const STOP_PREFIXES = ['from', 'via', 'to'] as const
|
|
66
|
+
type StopPrefix = (typeof STOP_PREFIXES)[number]
|
|
67
|
+
|
|
68
|
+
/** Tailwind's registered defaults; a position equal to it is not written. */
|
|
69
|
+
const DEFAULT_POSITIONS: Record<StopPrefix, number> = { from: 0, via: 50, to: 100 }
|
|
70
|
+
|
|
71
|
+
// Whole numbers without leading zeros, so every number has exactly one spelling.
|
|
72
|
+
const INTEGER = '(0|[1-9]\\d{0,2})'
|
|
73
|
+
const LINEAR = new RegExp(`^bg-linear-${INTEGER}$`)
|
|
74
|
+
const RADIAL_AT = new RegExp(`^bg-radial-\\[at_${INTEGER}%_${INTEGER}%\\]$`)
|
|
75
|
+
const STOP_HEX = /^(from|via|to)-\[(#[0-9A-Fa-f]{6}(?:[0-9A-Fa-f]{2})?)\]$/
|
|
76
|
+
const STOP_ROLE = new RegExp(`^(from|via|to)-theme-(${COLOR_ROLES.join('|')})$`)
|
|
77
|
+
const STOP_POSITION = new RegExp(`^(from|via|to)-${INTEGER}%$`)
|
|
78
|
+
|
|
79
|
+
const isStopPrefix = (value: string | undefined): value is StopPrefix =>
|
|
80
|
+
STOP_PREFIXES.some((prefix) => prefix === value)
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* A hex in the one spelling the contract writes: lower case, and without an
|
|
84
|
+
* opaque `ff` pair (`#AABBCCFF` → `#aabbcc`).
|
|
85
|
+
*/
|
|
86
|
+
export const normalizeGradientHex = (hex: `#${string}`): `#${string}` => {
|
|
87
|
+
const lower = hex.toLowerCase()
|
|
88
|
+
const normal = lower.length === 9 && lower.endsWith('ff') ? lower.slice(0, 7) : lower
|
|
89
|
+
return isHex(normal) ? normal : hex
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Reads a gradient class string, or returns `null` when it does not follow the
|
|
94
|
+
* contract: no type, no `from` or `to`, an unknown role or token, a value out
|
|
95
|
+
* of range, a class given twice, or stops whose positions run backwards (the
|
|
96
|
+
* contract always writes them in order). The order of the classes themselves
|
|
97
|
+
* is not checked. The result is in normal form, so serializing it gives the
|
|
98
|
+
* contract's spelling back.
|
|
99
|
+
*/
|
|
100
|
+
export const parseGradientClass = (value: string | undefined): GradientClass | null => {
|
|
101
|
+
if (!value) return null
|
|
102
|
+
|
|
103
|
+
let type: { type: 'linear'; angle: number } | { type: 'radial'; x: number; y: number } | null =
|
|
104
|
+
null
|
|
105
|
+
const colors: Partial<Record<StopPrefix, GradientClassColor>> = {}
|
|
106
|
+
const positions: Partial<Record<StopPrefix, number>> = {}
|
|
107
|
+
|
|
108
|
+
for (const token of value.trim().split(/\s+/)) {
|
|
109
|
+
const linear = LINEAR.exec(token)
|
|
110
|
+
const radialAt = RADIAL_AT.exec(token)
|
|
111
|
+
const hex = STOP_HEX.exec(token)
|
|
112
|
+
const role = STOP_ROLE.exec(token)
|
|
113
|
+
const position = STOP_POSITION.exec(token)
|
|
114
|
+
|
|
115
|
+
if (linear || radialAt || token === 'bg-radial') {
|
|
116
|
+
if (type) return null
|
|
117
|
+
if (linear) {
|
|
118
|
+
const angle = Number(linear[1])
|
|
119
|
+
if (angle > 359) return null
|
|
120
|
+
type = { type: 'linear', angle }
|
|
121
|
+
} else {
|
|
122
|
+
const x = radialAt ? Number(radialAt[1]) : 50
|
|
123
|
+
const y = radialAt ? Number(radialAt[2]) : 50
|
|
124
|
+
if (x > 100 || y > 100) return null
|
|
125
|
+
type = { type: 'radial', x, y }
|
|
126
|
+
}
|
|
127
|
+
} else if (hex || role) {
|
|
128
|
+
const prefix = (hex ?? role)?.[1]
|
|
129
|
+
if (!isStopPrefix(prefix) || colors[prefix]) return null
|
|
130
|
+
const captured = hex?.[2]
|
|
131
|
+
const stopRole = COLOR_ROLES.find((candidate) => candidate === role?.[2])
|
|
132
|
+
if (captured && isHex(captured)) colors[prefix] = { hex: normalizeGradientHex(captured) }
|
|
133
|
+
else if (stopRole) colors[prefix] = { role: stopRole }
|
|
134
|
+
else return null
|
|
135
|
+
} else if (position) {
|
|
136
|
+
const prefix = position[1]
|
|
137
|
+
const percent = Number(position[2])
|
|
138
|
+
if (!isStopPrefix(prefix) || positions[prefix] !== undefined || percent > 100) return null
|
|
139
|
+
positions[prefix] = percent
|
|
140
|
+
} else {
|
|
141
|
+
return null
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (!type || !colors.from || !colors.to) return null
|
|
146
|
+
if (positions.via !== undefined && !colors.via) return null
|
|
147
|
+
|
|
148
|
+
const stops = STOP_PREFIXES.flatMap((prefix) => {
|
|
149
|
+
const color = colors[prefix]
|
|
150
|
+
return color ? [{ color, position: positions[prefix] ?? DEFAULT_POSITIONS[prefix] }] : []
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
if (stops.some((stop, index) => index > 0 && stop.position < (stops[index - 1]?.position ?? 0)))
|
|
154
|
+
return null
|
|
155
|
+
|
|
156
|
+
return { ...type, stops }
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const colorClass = (prefix: StopPrefix, color: GradientClassColor) =>
|
|
160
|
+
'hex' in color
|
|
161
|
+
? `${prefix}-[${normalizeGradientHex(color.hex)}]`
|
|
162
|
+
: `${prefix}-theme-${color.role}`
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Writes a gradient in the contract's form. The stops are written in the order
|
|
166
|
+
* given, which must be their order on position: the first is `from`, the last
|
|
167
|
+
* `to`, and a third in between is `via`. Positions are rounded and clamped to
|
|
168
|
+
* 0–100, the angle to a whole 0–359 and the centre to whole percentages.
|
|
169
|
+
*/
|
|
170
|
+
export const serializeGradientClass = (gradient: GradientClass): EditorColorGradientValue => {
|
|
171
|
+
const { stops } = gradient
|
|
172
|
+
const prefixes: StopPrefix[] = stops.length === 3 ? ['from', 'via', 'to'] : ['from', 'to']
|
|
173
|
+
const percent = (value: number) => Math.min(100, Math.max(0, Math.round(value)))
|
|
174
|
+
|
|
175
|
+
const stopClasses = prefixes.flatMap((prefix, index) => {
|
|
176
|
+
const stop = index === prefixes.length - 1 ? stops[stops.length - 1] : stops[index]
|
|
177
|
+
if (!stop) return []
|
|
178
|
+
const position = percent(stop.position)
|
|
179
|
+
const color = colorClass(prefix, stop.color)
|
|
180
|
+
return position === DEFAULT_POSITIONS[prefix] ? [color] : [color, `${prefix}-${position}%`]
|
|
181
|
+
})
|
|
182
|
+
const rest = stopClasses.join(' ')
|
|
183
|
+
|
|
184
|
+
if (gradient.type === 'linear') {
|
|
185
|
+
const angle = ((Math.round(gradient.angle) % 360) + 360) % 360
|
|
186
|
+
return `bg-linear-${angle} ${rest}`
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const x = percent(gradient.x)
|
|
190
|
+
const y = percent(gradient.y)
|
|
191
|
+
return x === 50 && y === 50 ? `bg-radial ${rest}` : `bg-radial-[at_${x}%_${y}%] ${rest}`
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Whether a value reads as a gradient at all -- its first class is a gradient
|
|
196
|
+
* type -- even when the rest of it does not follow the contract. Such a value
|
|
197
|
+
* opens the picker in its gradient mode, with the placeholder in the trigger.
|
|
198
|
+
*/
|
|
199
|
+
export const looksLikeGradientClass = (value: string | undefined) =>
|
|
200
|
+
!!value && /^bg-(?:linear-|radial(?:\s|-\[|$))/.test(value.trim())
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* A parsed gradient class as a CSS `background-image`, for drawing it as an
|
|
204
|
+
* inline style: `linear-gradient(<angle>deg in oklab, …)`,
|
|
205
|
+
* `radial-gradient(at <x>% <y>% in oklab, …)`, or for a centred radial
|
|
206
|
+
* gradient `radial-gradient(in oklab, …)`. Always interpolated in oklab -- the
|
|
207
|
+
* same as the picker's preview -- and with every position written, 0, 50 and
|
|
208
|
+
* 100 included. Angle and positions are those of the Tailwind class, so a
|
|
209
|
+
* linear gradient renders exactly as `bg-linear-*` does. A hex stays a hex
|
|
210
|
+
* (eight digits with alpha); a role (`'main'`, `'accent'`, …) is resolved by
|
|
211
|
+
* `resolveRole`, for example to `var(--color-theme-main)`. Pure: no DOM, safe on the server.
|
|
212
|
+
*/
|
|
213
|
+
export const gradientClassToCss = (
|
|
214
|
+
gradient: GradientClass,
|
|
215
|
+
resolveRole: (role: EditorColorRole) => string,
|
|
216
|
+
): string =>
|
|
217
|
+
buildGradientCss(
|
|
218
|
+
gradient.type === 'linear'
|
|
219
|
+
? { type: 'linear', angle: gradient.angle }
|
|
220
|
+
: { type: 'radial', centerX: gradient.x, centerY: gradient.y },
|
|
221
|
+
gradient.stops.map(({ color, position }) => ({
|
|
222
|
+
color: 'hex' in color ? color.hex : resolveRole(color.role),
|
|
223
|
+
position,
|
|
224
|
+
})),
|
|
225
|
+
)
|
|
@@ -185,7 +185,9 @@ onMounted(async () => {
|
|
|
185
185
|
aria-hidden="true"
|
|
186
186
|
class="absolute inset-y-1 left-0 rounded-lg bg-white"
|
|
187
187
|
:class="
|
|
188
|
-
ready
|
|
188
|
+
ready
|
|
189
|
+
? 'transition-transform duration-500 ease-[cubic-bezier(0.34,1.2,0.64,1)] motion-reduce:transition-none'
|
|
190
|
+
: ''
|
|
189
191
|
"
|
|
190
192
|
:style="sliderStyle"
|
|
191
193
|
/>
|
|
@@ -242,7 +244,9 @@ onMounted(async () => {
|
|
|
242
244
|
aria-hidden="true"
|
|
243
245
|
class="pointer-events-none absolute inset-0 z-20 flex gap-1 p-1"
|
|
244
246
|
:class="[
|
|
245
|
-
ready
|
|
247
|
+
ready
|
|
248
|
+
? 'transition-[clip-path] duration-500 ease-[cubic-bezier(0.34,1.2,0.64,1)] motion-reduce:transition-none'
|
|
249
|
+
: '',
|
|
246
250
|
{ 'flex-wrap': !equalWidth },
|
|
247
251
|
]"
|
|
248
252
|
:style="{ clipPath }"
|