@nexxtmove/ui 2.2.0 → 2.3.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 (34) hide show
  1. package/dist/components/editor/molecules/EditorBackgroundSize/EditorBackgroundSize.vue.d.ts +2 -2
  2. package/dist/components/editor/molecules/EditorBlur/EditorBlur.vue.d.ts +2 -2
  3. package/dist/components/editor/molecules/EditorColor/EditorColor.vue.d.ts +44 -20
  4. package/dist/components/editor/molecules/EditorColor/_ColorField.vue.d.ts +66 -0
  5. package/dist/components/editor/molecules/EditorColor/colorClass.d.ts +46 -0
  6. package/dist/components/editor/molecules/EditorColor/gradientClass.d.ts +80 -0
  7. package/dist/components/editor/molecules/EditorGap/EditorGap.vue.d.ts +2 -2
  8. package/dist/components/editor/molecules/EditorMaxWidth/EditorMaxWidth.vue.d.ts +2 -2
  9. package/dist/components/editor/molecules/EditorShadow/EditorShadow.vue.d.ts +2 -2
  10. package/dist/components/organisms/ColorPicker/ColorPicker.vue.d.ts +58 -2
  11. package/dist/components/organisms/ColorPicker/_GradientField.vue.d.ts +23 -0
  12. package/dist/components/organisms/ColorPicker/_GradientPreview.vue.d.ts +11 -0
  13. package/dist/components/organisms/ColorPicker/_GradientStopRow.vue.d.ts +21 -0
  14. package/dist/components/organisms/ColorPicker/_GradientStops.vue.d.ts +19 -0
  15. package/dist/components/organisms/ColorPicker/gradient.d.ts +189 -0
  16. package/dist/components/organisms/ColorPicker/gradientGeometry.d.ts +101 -0
  17. package/dist/components/organisms/ColorPicker/useGradientEditor.d.ts +89 -0
  18. package/dist/index.d.ts +4 -1
  19. package/dist/index.js +3392 -2082
  20. package/package.json +1 -1
  21. package/src/components/editor/molecules/EditorColor/EditorColor.vue +83 -171
  22. package/src/components/editor/molecules/EditorColor/_ColorField.vue +445 -0
  23. package/src/components/editor/molecules/EditorColor/colorClass.ts +61 -0
  24. package/src/components/editor/molecules/EditorColor/gradientClass.ts +225 -0
  25. package/src/components/molecules/ToggleGroup/ToggleGroup.vue +6 -2
  26. package/src/components/organisms/ColorPicker/ColorPicker.vue +673 -271
  27. package/src/components/organisms/ColorPicker/_GradientField.vue +93 -0
  28. package/src/components/organisms/ColorPicker/_GradientPreview.vue +401 -0
  29. package/src/components/organisms/ColorPicker/_GradientStopRow.vue +181 -0
  30. package/src/components/organisms/ColorPicker/_GradientStops.vue +218 -0
  31. package/src/components/organisms/ColorPicker/gradient.ts +303 -0
  32. package/src/components/organisms/ColorPicker/gradientGeometry.ts +216 -0
  33. package/src/components/organisms/ColorPicker/useGradientEditor.ts +358 -0
  34. 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 ? 'transition-transform duration-500 ease-[cubic-bezier(0.34,1.2,0.64,1)]' : ''
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 ? 'transition-[clip-path] duration-500 ease-[cubic-bezier(0.34,1.2,0.64,1)]' : '',
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 }"