mnfst 0.5.162 → 0.5.164

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.
@@ -1,49 +1,15 @@
1
- /* Manifest Color Picker
2
-
3
- Directive: x-colorpicker[.<modifier>[="value"]]
4
-
5
- Root (no modifier): declares a picker container.
6
- Children (with modifier): declare hooks. Three families:
7
-
8
- • Layout (template OR instance — determined by host element tag)
9
- .solid canvas + hue/alpha sliders + value + format
10
- .gradient full gradient panel (layers container + layer template)
11
- .layer-options one gradient layer's UI
12
- .gradient-layers container holding cloned .layer-options instances
13
- .layer-stops-bar stops bar (inside a layer)
14
-
15
- • Actions (click triggers behavior)
16
- .add-layer .apply-color .grab-color
17
- .duplicate-layer .remove-layer
18
- .flip-layer .rotate-layer
19
- .duplicate-stop .delete-stop
20
- .set-gradient-type="linear|radial|conic"
21
-
22
- • Inputs (state-bound control)
23
- .set-canvas .set-hue .set-alpha
24
- .set-alpha-value .set-color-space .set-color-value
25
- .set-angle .set-gradient-value
26
-
27
- Magic: $colorpicker — returns the nearest ancestor picker's API
28
- for programmatic control (addLayer, setHue, layers, activeStop, ...).
29
- */
1
+ /* Manifest Color Picker — x-colorpicker directive + $colorpicker magic */
30
2
 
31
3
  function initializeColorpickerPlugin() {
32
4
 
33
5
  // ---- Shared global: ManifestUI (universal `_ui` resolver) ----
34
- // Defined guarded so the color picker localizes its default-menu chrome
35
- // whether or not the date picker / charts (which also define it) are loaded.
36
- // Kept byte-identical across those copies.
37
- //
38
- // `_ui` is a reserved, self-identifying key: any loaded data source may carry a
39
- // top-level `_ui` object, namespaced per element (`_ui.colorpicker`, `_ui.datepicker`).
40
- // No manifest flag — overrides piggyback on the normal local-data/localization model
41
- // and can be colocated with author content. resolve() deep-merges every loaded
42
- // source's `_ui[component]` onto the caller's English fallbacks.
6
+ // Guarded so the picker can localize its default-menu chrome whether or not
7
+ // the date picker / charts (which also define it) are loaded; byte-identical
8
+ // across those copies. Any loaded source may carry a top-level `_ui` object
9
+ // namespaced per element; resolve() deep-merges `_ui[component]` onto fallbacks.
43
10
  if (!window.ManifestUI) {
44
11
  window.ManifestUI = {
45
- // Names of data sources that have loaded (current locale). Enumerates loaded
46
- // sources only — never force-loads others just to scan them for `_ui`.
12
+ // Names of loaded data sources (current locale); never force-loads others.
47
13
  _loadedSourceNames() {
48
14
  try {
49
15
  const store = window.ManifestDataStore && window.ManifestDataStore.rawDataStore;
@@ -152,10 +118,8 @@ function initializeColorpickerPlugin() {
152
118
 
153
119
  function roundA(a) { const v=Math.round(a*100); return v===100?'1':(v/100).toString(); }
154
120
 
155
- // Canonical dedupe key for a color or gradient value. Used to tag library swatches
156
- // with a `data-cp-key` that the picker can match against its current color to toggle
157
- // an `active` class. Parses any CSS color via parseCssColor → 8-digit hex. Gradients
158
- // are compared as their normalized CSS string.
121
+ // Canonical dedupe key for a color/gradient 8-digit hex for colors, the CSS
122
+ // string for gradients. Tags library swatches with data-cp-key for active-toggle.
159
123
  function _swatchKeyOf(value) {
160
124
  if (typeof value !== 'string') return null;
161
125
  const v = value.trim();
@@ -194,8 +158,7 @@ function initializeColorpickerPlugin() {
194
158
  // ---- Canvas (SV plane) ----
195
159
 
196
160
  function drawSvCanvas(canvas, hue) {
197
- // Cache the 2D context on the canvas element. No `willReadFrequently` —
198
- // we only ever write to this canvas, so we want GPU-accelerated compositing.
161
+ // Cached context; no willReadFrequently (write-only GPU compositing).
199
162
  const ctx = canvas._cpCtx || (canvas._cpCtx = canvas.getContext('2d'));
200
163
  const w = canvas.width, h = canvas.height;
201
164
  const hr = hsvToRgb(hue, 100, 100);
@@ -226,11 +189,9 @@ function initializeColorpickerPlugin() {
226
189
  return layers.map(buildLayerString).join(', ');
227
190
  }
228
191
 
229
- // ---- Gradient parsing (reverse of buildLayerString / buildFullGradientString) ----
230
- //
231
- // Parses a CSS gradient string back into the layers/stops structure the picker
232
- // uses internally. Tolerant of whatever the browser serializes from inline
233
- // styles (rgb(...) and rgba(...) instead of hex, etc.).
192
+ // ---- Gradient parsing (reverse of the builders) ----
193
+ // Parses a CSS gradient string back into layers/stops. Tolerant of whatever
194
+ // the browser serializes from inline styles (rgb()/rgba() instead of hex).
234
195
 
235
196
  // Split a string on top-level commas (commas not inside parentheses).
236
197
  function _splitTopLevelCommas(str) {
@@ -246,9 +207,7 @@ function initializeColorpickerPlugin() {
246
207
  return out;
247
208
  }
248
209
 
249
- // Split a string into individual gradient calls top-level segments that each
250
- // start with `<type>-gradient(`. Handles multi-layer gradients like
251
- // "linear-gradient(...), radial-gradient(...)".
210
+ // Split into individual gradient calls (top-level `<type>-gradient(` segments).
252
211
  function _splitGradientLayers(str) {
253
212
  const segments = _splitTopLevelCommas(str);
254
213
  return segments.filter(s => /^(linear|radial|conic)-gradient\s*\(/i.test(s));
@@ -415,10 +374,8 @@ function initializeColorpickerPlugin() {
415
374
  function buildIosPreset(labels) {
416
375
  const L = labels || {};
417
376
  const groupName = L._group || 'iOS';
418
- // Swatch names are prefixed with the tone ("Light - " / "Dark - ") so users
419
- // can still distinguish them by name once a color lands in Recent (where
420
- // palette context is lost). The `_lightPrefix` / `_darkPrefix` meta keys
421
- // let devs translate or remove the prefixes.
377
+ // Tone prefix ("Light - " / "Dark - ") keeps swatches distinguishable in
378
+ // Recent, where palette context is lost. `_lightPrefix`/`_darkPrefix` override.
422
379
  const lightPrefix = L._lightPrefix != null ? L._lightPrefix : 'Light - ';
423
380
  const darkPrefix = L._darkPrefix != null ? L._darkPrefix : 'Dark - ';
424
381
  const baseColorName = (c) => L[c.name] || titleCase(c.name);
@@ -446,12 +403,10 @@ function initializeColorpickerPlugin() {
446
403
 
447
404
  // ---- Library data normalization ----
448
405
 
449
- // Normalize into: Group[] where each Group = { name?, colors?: Swatch[], palettes?: Palette[], ...extras }
450
- // A Group has EITHER flat `colors` OR nested `palettes`, never both. Each Palette = { name?, colors: Swatch[], ...extras }.
451
- // Swatch = { name?, value, ...extras }. Hierarchy is strictly Group > Palette > Swatch (no deeper nesting).
452
- // Filter proxy/magic noise: drop $-prefixed keys (Manifest magic like $route, $x, $watch),
453
- // symbol keys, function values, and the few inherited Object.prototype names that can leak
454
- // through exotic proxies.
406
+ // Normalize into Group[] Group = { name?, colors?|palettes? }, Palette =
407
+ // { name?, colors }, Swatch = { name?, value }. Hierarchy is strictly
408
+ // Group > Palette > Swatch. Filters $-magic, symbol keys, functions, and
409
+ // Object.prototype names that leak through proxies.
455
410
  const _LIB_PROTO_NOISE = new Set(['valueOf', 'toString', 'constructor', 'hasOwnProperty', 'isPrototypeOf', 'propertyIsEnumerable', 'toLocaleString']);
456
411
  function _cleanLibraryEntries(input) {
457
412
  return Object.entries(input).filter(([k, v]) => {
@@ -519,9 +474,8 @@ function initializeColorpickerPlugin() {
519
474
  return { name: groupName, colors: [] };
520
475
  }
521
476
 
522
- // Merge built-in Recent + Tailwind + iOS into a single default data object.
523
- // Used when `x-colorpicker.library` has no expression AND no ancestor $x data.
524
- // Optional `labels` is forwarded to the Tailwind / iOS builders for localization.
477
+ // Built-in Recent + Tailwind + iOS default library. Used when
478
+ // x-colorpicker.library has no expression and no ancestor $x data.
525
479
  function buildDefaultLibrary(labels) {
526
480
  const L = labels || {};
527
481
  return {
@@ -532,32 +486,13 @@ function initializeColorpickerPlugin() {
532
486
  }
533
487
 
534
488
  // ---- Auto-discovery of colorpicker data sources ----
535
- //
536
- // Devs register data sources in manifest.json using the standard Manifest conventions,
537
- // then flag which ones contain color library content via a top-level `colorpicker` entry:
538
- //
539
- // {
540
- // "data": {
541
- // "myColors": "/data/colors.yaml" // non-localized
542
- // "brand": { "en": "/data/brand.en.yaml", "fr": "..." } // per-locale
543
- // },
544
- // "colorpicker": "myColors" // single source
545
- // "colorpicker": ["myColors", "brand"] // multiple sources
546
- // }
547
- //
548
- // The colorpicker plugin scans $x.manifest.data for entries flagged with a
549
- // `colorpicker:` key, then merges the loaded $x.<name> values in declaration
550
- // order. Each source may contain:
551
- // _tailwind: (optional) replaces the built-in Tailwind group entirely
552
- // _ios: (optional) replaces the built-in iOS group entirely
553
- // <Any Name>: custom groups appended to the library after built-ins
554
- //
555
- // Presence of _tailwind / _ios is "all or nothing" — no merge with built-in data.
556
- // If multiple flagged sources both include _tailwind, the LAST source wins.
489
+ // manifest.json flags library sources via a top-level `colorpicker` key (one
490
+ // name or an array). Each source may carry `_tailwind` / `_ios` (all-or-nothing
491
+ // replacements of the built-in group, last wins) plus custom named groups.
492
+ // See colorpickers.md for the manifest shape.
557
493
 
558
- // Manifest's data proxy returns an empty object for ANY property access (graceful chain),
559
- // so `src._tailwind` always looks truthy. We only treat an override as real if the source
560
- // actually declared the key (`in` operator) AND the value has real content.
494
+ // Manifest's data proxy returns {} for any access, so `src._tailwind` always
495
+ // looks truthy. Only treat an override as real if declared (`in`) with content.
561
496
  function _hasRealContent(v) {
562
497
  if (v == null || typeof v !== 'object') return false;
563
498
  return Object.keys(v).some(k => !k.startsWith('$')
@@ -565,8 +500,8 @@ function initializeColorpickerPlugin() {
565
500
  && k !== 'contentType');
566
501
  }
567
502
 
568
- // Takes an array of resolved source values, extracts _tailwind / _ios overrides and
569
- // custom groups from each, and returns the final normalized groups array.
503
+ // Extract _tailwind / _ios overrides + custom groups from resolved sources,
504
+ // returning the final normalized groups array.
570
505
  function composeLibraryFromSources(sources) {
571
506
  let tailwindOverride = null;
572
507
  let iosOverride = null;
@@ -593,14 +528,13 @@ function initializeColorpickerPlugin() {
593
528
  const groups = [];
594
529
  if (recent.length) {
595
530
  const recentGroup = normalizeGroup(recent, 'Recent');
596
- recentGroup._isRecent = true; // marker — renderer picks the `library-recent-swatch` template over `library-swatch`
531
+ recentGroup._isRecent = true; // renderer picks library-recent-swatch template
597
532
  groups.push(recentGroup);
598
533
  }
599
534
 
600
535
  groups.push(...customGroups);
601
536
 
602
537
  if (tailwindOverride) {
603
- // `_group` meta inside the override becomes the group heading; default to "Tailwind"
604
538
  groups.push(normalizeGroup(tailwindOverride, tailwindOverride._group || 'Tailwind'));
605
539
  } else {
606
540
  groups.push(...normalizeLibraryInput(buildTailwindPreset()));
@@ -615,20 +549,10 @@ function initializeColorpickerPlugin() {
615
549
  return groups;
616
550
  }
617
551
 
618
- // Walk a normalized groups array and resolve any reference-style strings in
619
- // user-facing labels (group / palette / swatch names) against Alpine's scope.
620
- // Two reference shapes are recognized:
621
- // Bare path: "$x.colorLabels.primary" → Alpine.evaluate(...)
622
- // • Template: "${$locale.t('brand.primary')}" → Alpine.evaluate(...) as a literal
623
- // Plain strings pass through unchanged. Failed lookups return the original
624
- // string so a missing key surfaces as-is rather than rendering empty.
625
- //
626
- // Reading via Alpine.evaluate inside the surrounding render effect registers
627
- // reactive deps on the referenced data — locale switches and content updates
628
- // re-trigger the render automatically.
629
- // Resolve a single `$x.`/`$locale`/`${…}` reference string against document.body
630
- // scope. Plain strings pass through unchanged; returns the original on any failure.
631
- // Reading via Alpine.evaluate inside a render effect registers the locale dep.
552
+ // Resolve a `$x.`/`$locale`/`${…}` reference string against document.body scope.
553
+ // Recognizes bare paths and template literals; plain strings and failures pass
554
+ // through unchanged. Alpine.evaluate inside the render effect registers the
555
+ // locale/content dep so switches re-trigger the render.
632
556
  function _resolveRefString(val) {
633
557
  if (typeof val !== 'string' || val.length === 0) return val;
634
558
  const trimmed = val.trim();
@@ -671,18 +595,16 @@ function initializeColorpickerPlugin() {
671
595
  return groups;
672
596
  }
673
597
 
674
- // Returns an object that is BOTH callable (for localization) AND spreadable (for composition).
675
- // `builder` is a function that takes optional labels and returns a preset object.
676
- // Spreading the returned value exposes the default (unlabeled) preset's top-level keys.
598
+ // Returns an object both callable (for localization) and spreadable (for
599
+ // composition) spreading exposes the default preset's top-level keys.
677
600
  function _makeCallablePreset(builder) {
678
601
  const fn = (labels) => builder(labels);
679
- Object.assign(fn, builder()); // default preset's keys become own enumerable props on fn
602
+ Object.assign(fn, builder());
680
603
  return fn;
681
604
  }
682
605
 
683
- // Render a group by cloning the group template and expanding nested templates in place.
684
- // Returns a fragment with all top-level elements (scope applied to the first). `isRecent`
685
- // threads down so palette/swatch layers can pick the Recent-specific swatch template.
606
+ // Render a group by cloning its template and expanding nested templates in
607
+ // place. `isRecent` threads down to pick the Recent-specific swatch template.
686
608
  function renderLibraryGroup(groupTpl, group) {
687
609
  const frag = groupTpl.content.cloneNode(true);
688
610
  const primary = frag.firstElementChild;
@@ -691,7 +613,7 @@ function initializeColorpickerPlugin() {
691
613
 
692
614
  const isRecent = !!group._isRecent;
693
615
 
694
- // Normalize: flat groups become single unnamed palette so templates work uniformly
616
+ // Flat groups become a single unnamed palette so templates work uniformly.
695
617
  const palettes = (group.palettes && group.palettes.length)
696
618
  ? group.palettes
697
619
  : (group.colors && group.colors.length ? [{ name: null, colors: group.colors }] : []);
@@ -733,23 +655,19 @@ function initializeColorpickerPlugin() {
733
655
  return frag;
734
656
  }
735
657
 
736
- // Per-clone counter for uniquifying nested dropdown/menu ids mirrors the scheme
737
- // used for gradient layer clones. Only menus that live INSIDE the swatch template
738
- // get uniquified; menus placed at the library root are left alone (shared).
658
+ // Per-clone counter for uniquifying nested dropdown/menu ids. Only menus
659
+ // inside the swatch template get uniquified; library-root menus stay shared.
739
660
  let _swatchCloneCounter = 0;
740
661
 
741
662
  function renderLibrarySwatch(swatchTpl, swatch) {
742
663
  const frag = swatchTpl.content.cloneNode(true);
743
664
  const primary = frag.firstElementChild;
744
665
  if (!primary) return frag;
745
- // Apply the swatch scope to the primary element (typically the swatch button).
746
- // Sibling elements like nested menus don't need swatch scope — their actions
747
- // read from the dropdowns plugin's trigger ref.
666
+ // Swatch scope on the primary (the swatch button); nested menus read from
667
+ // the dropdowns plugin's trigger ref instead.
748
668
  primary.setAttribute('x-data', '{ swatch: ' + _jsonStringifyForAlpine(swatch) + ' }');
749
- // Raw value + canonical key go on BOTH the actual apply-color element AND the
750
- // primary wrapper (if different). That way `_updateActiveSwatches` can toggle
751
- // `.active` on both, and dev CSS targeting either selector — the wrapper div
752
- // (for layout effects like `order: -1`) or the button (for box-shadow) — works.
669
+ // Value + key go on both apply-color element and primary wrapper so
670
+ // _updateActiveSwatches can toggle `.active` and dev CSS can target either.
753
671
  if (swatch && typeof swatch.value === 'string') {
754
672
  const applyEl = frag.querySelector('[x-colorpicker\\.apply-color]');
755
673
  const key = _swatchKeyOf(swatch.value);
@@ -761,14 +679,13 @@ function initializeColorpickerPlugin() {
761
679
  if (key) t.setAttribute('data-cp-key', key);
762
680
  }
763
681
  }
764
- // Uniquify any nested dropdown/menu ids so per-swatch context menus don't
765
- // collide. Skipped automatically when the menu lives outside the template.
682
+ // Uniquify nested menu ids so per-swatch context menus don't collide.
766
683
  uniquifyDropdownIdsIn(frag, 'swatch-' + (++_swatchCloneCounter));
767
684
  return frag;
768
685
  }
769
686
 
770
- // Default fallback: if no library template supplied, render a small heading per group
771
- // and a flex-wrap row of apply-color spans for each swatch.
687
+ // Fallback when no library template supplied: heading per group + a flex-wrap
688
+ // row of apply-color spans.
772
689
  function renderDefaultGroup(group) {
773
690
  const root = document.createElement('div');
774
691
  root.setAttribute('data-cp-library-group', group.name || '');
@@ -804,12 +721,10 @@ function initializeColorpickerPlugin() {
804
721
  if (input == null) return { name: paletteName, colors: [] };
805
722
  if (Array.isArray(input)) return { name: paletteName, colors: normalizeColorList(input) };
806
723
  if (typeof input === 'object') {
807
- // Meta key `_name` overrides the display name (lets devs keep object keys stable
808
- // across locales while translating visible text).
724
+ // `_name` meta overrides the display name (keeps object keys stable across locales).
809
725
  const displayName = (typeof input._name === 'string') ? input._name : (input.name || paletteName);
810
- // Explicit `_colors` / `colors` / `items` key holds the swatch list — use when
811
- // you need to pair `_name` with an array of bare hex strings (YAML can't mix
812
- // object keys and sequence items in the same node).
726
+ // Explicit `_colors`/`colors`/`items` holds the swatch list — pairs
727
+ // `_name` with a bare-hex array (YAML can't mix keys and sequence items).
813
728
  if ('_colors' in input || 'colors' in input || 'items' in input) {
814
729
  return {
815
730
  ...input,
@@ -844,7 +759,7 @@ function initializeColorpickerPlugin() {
844
759
  return input.map(item => _coerceSwatch(item)).filter(Boolean);
845
760
  }
846
761
  if (typeof input === 'object') {
847
- // Filter `_`-prefixed meta keys (e.g., `_name`, `_group`) so they're not mistaken for shades.
762
+ // Filter `_`-prefixed meta keys so they're not mistaken for shades.
848
763
  return Object.entries(input)
849
764
  .filter(([k]) => !k.startsWith('_'))
850
765
  .map(([name, value]) => _coerceSwatch(value, name))
@@ -896,8 +811,7 @@ function initializeColorpickerPlugin() {
896
811
 
897
812
  // ---- Default fallback templates (used when developer doesn't supply their own) ----
898
813
 
899
- // Solid-options panel — used both as a top-level tab body and as the
900
- // accordion content under each gradient stop.
814
+ // Solid-options panel — top-level tab body and per-stop accordion content.
901
815
  const DEFAULT_SOLID_TEMPLATE_HTML = `
902
816
  <div>
903
817
  <div class="canvas-wrapper">
@@ -924,10 +838,8 @@ function initializeColorpickerPlugin() {
924
838
  </div>
925
839
  `;
926
840
 
927
- // Gradient layer template one of these per layer clone. Includes the
928
- // gradient-type dropdown (with reactive icon class), angle input, stops bar
929
- // with right-click context menu (Duplicate/Delete + inline library), and
930
- // the per-stop solid-panel accordion.
841
+ // Gradient layer template (one per layer clone) type dropdown, angle input,
842
+ // stops bar with context menu, and the per-stop solid-panel accordion.
931
843
  const DEFAULT_LAYER_TEMPLATE_HTML = `
932
844
  <div>
933
845
  <div class="layer-options-wrapper">
@@ -1067,14 +979,9 @@ function initializeColorpickerPlugin() {
1067
979
  const _defaultLibraryLayoutTpl = parseOnce(DEFAULT_LIBRARY_LAYOUT_HTML);
1068
980
 
1069
981
  // ---- Default-menu text (localizable via `_ui`) ----
1070
- //
1071
- // English defaults for the chrome baked into the default menu. A project
1072
- // overrides any subset by flagging a data source with a `colorpicker` key and
1073
- // giving it a top-level `_ui` object (see colorpickers.md → Text & Localization).
1074
- // window.ManifestUI.resolve deep-overlays those overrides onto these fallbacks;
1075
- // values may be plain strings or `$x`/`$locale` references. The English text also
1076
- // lives verbatim in the templates above, so the menu still reads correctly if the
1077
- // resolver never runs (e.g. ManifestUI absent). Key shape matches the docs exactly.
982
+ // English fallbacks; ManifestUI.resolve deep-overlays project `_ui` overrides.
983
+ // The same English also lives verbatim in the templates above, so the menu
984
+ // reads correctly even if the resolver never runs.
1078
985
  const UI_FALLBACK = {
1079
986
  tabs: { solid: 'Solid', gradient: 'Gradient', library: 'Library' },
1080
987
  grabColor: 'Grab color',
@@ -1107,10 +1014,9 @@ function initializeColorpickerPlugin() {
1107
1014
  return ui;
1108
1015
  }
1109
1016
 
1110
- // Stamp a resolved `_ui` object onto a freshly-cloned default-template subtree.
1111
- // Text nodes carry data-cp-ui-text="dotted.path"; icon-only buttons whose only
1112
- // label is a tooltip/aria carry data-cp-ui-label="dotted.path". Dev-authored
1113
- // custom templates have no markers, so this is a no-op for them.
1017
+ // Stamp a resolved `_ui` object onto a cloned default-template subtree via
1018
+ // data-cp-ui-text (textContent) / data-cp-ui-label (aria+tooltip) markers.
1019
+ // No-op for dev-authored templates (no markers).
1114
1020
  function _applyDefaultUiText(scopeEl, ui) {
1115
1021
  if (!scopeEl || !ui) return;
1116
1022
  scopeEl.querySelectorAll('[data-cp-ui-text]').forEach(el => {
@@ -1130,12 +1036,11 @@ function initializeColorpickerPlugin() {
1130
1036
 
1131
1037
  let pickerCounter = 0;
1132
1038
 
1133
- // Reactive registry of pickers keyed by element ID.
1134
- // Consumers read via the magic; reads are tracked even for not-yet-registered IDs,
1135
- // so bindings resolve correctly when a picker mounts later in the DOM.
1039
+ // Reactive registry of pickers keyed by element ID. Reads track unregistered
1040
+ // IDs too, so bindings resolve when a picker mounts later.
1136
1041
  const _pickerRegistry = window.Alpine?.reactive ? Alpine.reactive({}) : {};
1137
1042
 
1138
- // Fallback API used when a picker ID isn't registered yet; coerces to empty string.
1043
+ // Fallback API for an unregistered picker ID; coerces to empty string.
1139
1044
  const _nullApi = (() => {
1140
1045
  const noop = () => {};
1141
1046
  const empty = () => '';
@@ -1174,17 +1079,13 @@ function initializeColorpickerPlugin() {
1174
1079
  pickerMode: 'solid',
1175
1080
  openStop: null, // { layerIndex, stopIndex } | null
1176
1081
 
1177
- // Tab/panel filtering. When set (via array-literal directive expression
1178
- // on the root, e.g. `x-colorpicker="['solid', 'gradient']"`), the default
1179
- // injected UI is filtered + reordered to match. null/empty = all panels.
1082
+ // Panel filter from an array-literal root expression (e.g.
1083
+ // x-colorpicker="['solid','gradient']"); null = all panels.
1180
1084
  allowedPanels: null,
1181
1085
 
1182
- // Elements that act as live labels for the current solid format (e.g. a
1183
- // dropdown button whose text reads "Hex"/"RGB"/"HSL"/"OKLCH"). Refreshed
1184
- // whenever the format changes via _refreshFormatLabels().
1086
+ // Live format-label elements (dropdown button text "Hex"/"RGB"/...).
1185
1087
  formatLabelEls: [],
1186
- // Elements bound to a specific format value (e.g. <li set-color-space="hex">).
1187
- // Refreshed alongside the labels to toggle an `active` class on the current.
1088
+ // Elements bound to a specific format value (toggle `active` on current).
1188
1089
  formatChoiceEls: [],
1189
1090
 
1190
1091
  // Element registry (populated by child directive handlers)
@@ -1199,27 +1100,23 @@ function initializeColorpickerPlugin() {
1199
1100
  gradientValueInputs: [], // <textarea x-colorpicker.set-gradient-value>
1200
1101
 
1201
1102
  // Library
1202
- libraryContainers: [], // <div x-colorpicker.library> — every registered target (top-level tab, nested stop menus, etc.)
1203
- libraryTemplate: null, // <template x-colorpicker.library> — dev layout (cloned into container)
1204
- libraryRootValue: null, // expression from x-colorpicker.library="..." (data source)
1205
-
1206
- // Recent-list commit tracking. `_recentBaseline` is the color at the start of
1207
- // a commit cycle (popover open, or inline picker init / last commit).
1208
- // `_lastChangeFromLibrary` is true if the most recent user change was picking
1209
- // a preset swatch — those don't count as "recent" even if committed.
1210
- // `_hasUserChange` is true if any non-library user interaction has happened
1211
- // since the baseline was set.
1103
+ libraryContainers: [], // <div x-colorpicker.library> — every registered target
1104
+ libraryTemplate: null, // <template x-colorpicker.library> — dev layout
1105
+ libraryRootValue: null, // x-colorpicker.library="..." data source expression
1106
+
1107
+ // Recent-list commit tracking. Baseline = color at cycle start;
1108
+ // _lastChangeFromLibrary = preset swatch (doesn't count as recent);
1109
+ // _hasUserChange = any non-library interaction since baseline.
1212
1110
  _recentBaseline: null,
1213
1111
  _lastChangeFromLibrary: false,
1214
1112
  _hasUserChange: false,
1215
1113
 
1216
- // The currently active solid-controls refs (inside Solid tab OR open-stop accordion)
1114
+ // Active solid-controls refs (Solid tab or open-stop accordion)
1217
1115
  activeControls: null,
1218
1116
  solidTabRefs: null,
1219
1117
 
1220
- // Reactive version counter — bumped on every state change. The API
1221
- // getters read this to establish a reactive dependency, then compute
1222
- // values lazily. No upfront work when nothing is bound to $colorpicker(id).
1118
+ // Reactive version counter — bumped on every change; API getters read
1119
+ // it to establish a dep, then compute lazily.
1223
1120
  snapshot: window.Alpine?.reactive ? Alpine.reactive({ version: 0 }) : { version: 0 },
1224
1121
 
1225
1122
  // ---- Active target accessors ----
@@ -1276,8 +1173,7 @@ function initializeColorpickerPlugin() {
1276
1173
  this.activeLayerIndex = 0;
1277
1174
  this.activeStopIndex = 0;
1278
1175
  this.openStop = null;
1279
- // Seed the picker's working solid color from the first stop so
1280
- // syncUI / hex output / canvas reflect something coherent.
1176
+ // Seed the working solid color from the first stop.
1281
1177
  const firstStop = layers[0].stops[0];
1282
1178
  if (firstStop) {
1283
1179
  this.h = firstStop.color.h; this.s = firstStop.color.s;
@@ -1308,8 +1204,8 @@ function initializeColorpickerPlugin() {
1308
1204
  this.activeLayerIndex = layerIndex;
1309
1205
  this.activeStopIndex = stopIndex;
1310
1206
  this.renderLayers();
1311
- // Gradient swatches in the library must be disabled while a stop is open
1312
- // (CSS gradients can't contain nested gradients as stop colors).
1207
+ // Library gradient swatches disabled while a stop is open
1208
+ // (CSS can't nest gradients as stop colors).
1313
1209
  this._syncStopGradientDisable();
1314
1210
  },
1315
1211
 
@@ -1428,8 +1324,7 @@ function initializeColorpickerPlugin() {
1428
1324
  },
1429
1325
 
1430
1326
  applyColor(str) {
1431
- // Pure setter — Recent-list commits happen at the picker-close boundary
1432
- // (popover close / inline focusout), not on every call.
1327
+ // Pure setter — Recent commits happen at the close boundary, not here.
1433
1328
  if (this.setFromString(str)) {
1434
1329
  this.syncUI();
1435
1330
  this.syncToInput();
@@ -1437,10 +1332,8 @@ function initializeColorpickerPlugin() {
1437
1332
  }
1438
1333
  },
1439
1334
 
1440
- // Toggle `.active` on library swatches whose canonical key matches the
1441
- // picker's current color. Gradients compare as their full CSS string;
1442
- // solids compare as 8-digit hex. Runs across every registered library
1443
- // container (tab + any nested containers such as stop context menus).
1335
+ // Toggle `.active` on library swatches matching the current color's key,
1336
+ // across every registered library container.
1444
1337
  _updateActiveSwatches() {
1445
1338
  if (!this.libraryContainers.length) return;
1446
1339
  const current = this.isGradient
@@ -1458,14 +1351,9 @@ function initializeColorpickerPlugin() {
1458
1351
  this._syncStopGradientDisable();
1459
1352
  },
1460
1353
 
1461
- // Gradient-valued library swatches must be un-pickable when the click would
1462
- // try to apply that gradient as a gradient-stop color (CSS doesn't allow
1463
- // nested gradients in stop positions). That's the case when:
1464
- // • a stop accordion is currently open (openStop set), OR
1465
- // • the library container itself is nested inside a stop-context-menu —
1466
- // every click in such a menu targets the right-clicked stop's color.
1467
- // Dev CSS styles [disabled] on apply-color elements; the click handler
1468
- // also short-circuits as belt-and-braces.
1354
+ // Disable gradient library swatches when a click would apply them as a
1355
+ // stop color (can't nest gradients): a stop accordion is open, or the
1356
+ // container is inside a stop-context-menu. Dev CSS styles [disabled].
1469
1357
  _syncStopGradientDisable() {
1470
1358
  const stopIsOpen = !!this.openStop;
1471
1359
  for (const c of this.libraryContainers) {
@@ -1484,14 +1372,10 @@ function initializeColorpickerPlugin() {
1484
1372
  },
1485
1373
 
1486
1374
  // ---- Recent-list commit cycle ----
1487
- //
1488
- // Rules:
1489
- // Popover pickers commit on toggle→closed.
1490
- // Inline pickers commit on focusout past the rootEl.
1491
- // • A commit pushes the current color to Recent IFF the user made a
1492
- // non-library change since the last baseline and the color actually differs.
1493
- // • Gradient mode never commits (stops are part of a gradient, not standalone).
1494
- // • Programmatic api.applyColor calls don't mark user changes — only UI paths do.
1375
+ // Popover commits on close, inline on focusout past rootEl. A commit
1376
+ // pushes to Recent only if a non-library user change since baseline
1377
+ // actually changed the color. Gradient mode never commits; programmatic
1378
+ // applyColor doesn't mark a user change.
1495
1379
 
1496
1380
  _startCommitCycle() {
1497
1381
  this._recentBaseline = this._currentCommitValue();
@@ -1521,10 +1405,8 @@ function initializeColorpickerPlugin() {
1521
1405
  this._startCommitCycle();
1522
1406
  },
1523
1407
 
1524
- // Shared-picker write-back: when a swatch with x-model triggered this picker,
1525
- // push the current color through the swatch's model setter on commit. Unlike
1526
- // Recent, this runs even when the change came from a library click — the user
1527
- // clicked a library swatch intending to assign that color to their field.
1408
+ // Write the current color back through the trigger's x-model setter on
1409
+ // commit. Runs even for library clicks (the user meant to assign it).
1528
1410
  _commitToTrigger() {
1529
1411
  const trigger = this.triggerBtn;
1530
1412
  if (!trigger || typeof trigger._cpModelSetter !== 'function') return;
@@ -1537,10 +1419,7 @@ function initializeColorpickerPlugin() {
1537
1419
  if (!window.EyeDropper) return;
1538
1420
  try {
1539
1421
  const result = await new EyeDropper().open();
1540
- // Eyedropper is a single-color operation — route the result
1541
- // to Solid mode and surface the solid controls. Mark as a
1542
- // non-library user change; the usual commit boundary decides
1543
- // whether it lands in Recent (user may tweak before closing).
1422
+ // Single-color op — route to Solid mode, mark a non-library change.
1544
1423
  this._switchToSolidMode();
1545
1424
  this._markUserChange(false);
1546
1425
  this.applyColor(result.sRGBHex);
@@ -1560,9 +1439,8 @@ function initializeColorpickerPlugin() {
1560
1439
  if (this.layersContainer) this.renderLayers();
1561
1440
  },
1562
1441
 
1563
- // Switch picker mode without touching the dev's `tab` Alpine data.
1564
- // Used by library-tab applies — the user is browsing swatches and
1565
- // shouldn't be flipped off the Library tab when they pick one.
1442
+ // Switch mode without touching the dev's `tab` data — library-tab
1443
+ // applies shouldn't flip the user off the Library tab.
1566
1444
  _setPickerMode(mode) {
1567
1445
  if (this.pickerMode === mode) return;
1568
1446
  this.pickerMode = mode;
@@ -1603,9 +1481,8 @@ function initializeColorpickerPlugin() {
1603
1481
  }
1604
1482
  },
1605
1483
 
1606
- // Sync any registered format label elements to the current format and
1607
- // toggle an `active` class on each format-choice element so devs can
1608
- // style the active option without writing reactive bindings.
1484
+ // Sync registered format-label elements and toggle `active` on each
1485
+ // format-choice element for the current format.
1609
1486
  _refreshFormatLabels() {
1610
1487
  const current = this.format;
1611
1488
  const label = this._formatLabel(current);
@@ -1641,10 +1518,8 @@ function initializeColorpickerPlugin() {
1641
1518
  syncToInput() {
1642
1519
  const swatchVal = this.toSwatchColor();
1643
1520
  if (this.hiddenInput) {
1644
- // Native <input type="color"> only accepts #rrggbb; everything
1645
- // else (synthesized type=hidden, dev-supplied type=hidden) gets
1646
- // the full CSS string — solid color or gradient — so gradients
1647
- // and non-hex formats round-trip without lossy conversion.
1521
+ // Native <input type=color> only accepts #rrggbb; hidden inputs
1522
+ // get the full CSS string so gradients/non-hex round-trip losslessly.
1648
1523
  const isNativeColorInput = this.hiddenInput.type === 'color';
1649
1524
  this.hiddenInput.value = isNativeColorInput ? this.toHex() : swatchVal;
1650
1525
  this.hiddenInput.dispatchEvent(new Event('input', { bubbles: true }));
@@ -1652,15 +1527,10 @@ function initializeColorpickerPlugin() {
1652
1527
  }
1653
1528
  if (this.triggerBtn) this.triggerBtn.style.setProperty('--color-picker-swatch', swatchVal);
1654
1529
  this.updateGradientValue();
1655
- // Reflect current color in the library: any swatch whose canonical key
1656
- // matches gets an `.active` class, all others lose it.
1657
1530
  this._updateActiveSwatches();
1658
- // Refresh any custom format labels / choice highlights — covers paths
1659
- // where format changes implicitly (setFromString parsing a new format).
1660
1531
  this._refreshFormatLabels();
1661
1532
 
1662
- // Bump reactive version — any $colorpicker(id).* reader re-runs.
1663
- // No eager computation of hex/css/etc. unless somebody is actually bound to them.
1533
+ // Bump reactive version — $colorpicker(id).* readers re-run (lazily).
1664
1534
  this.snapshot.version++;
1665
1535
  },
1666
1536
 
@@ -1676,11 +1546,11 @@ function initializeColorpickerPlugin() {
1676
1546
  const canvas = this.activeControls?.canvas;
1677
1547
  if (!canvas) return;
1678
1548
  const rect = canvas.getBoundingClientRect();
1679
- if (rect.width <= 0) return; // not visible yet; ResizeObserver will redraw when it gains size
1549
+ if (rect.width <= 0) return; // not visible; ResizeObserver redraws later
1680
1550
  let dimsChanged = false;
1681
1551
  if (canvas.width !== rect.width) { canvas.width = rect.width; dimsChanged = true; }
1682
1552
  if (canvas.height !== rect.height) { canvas.height = rect.height; dimsChanged = true; }
1683
- // Skip repaint if hue unchanged and dimensions unchanged (setting canvas.* dims clears the pixels)
1553
+ // Skip repaint if hue and dims unchanged (setting dims clears pixels).
1684
1554
  if (!dimsChanged && canvas._cpLastHue === this.h) return;
1685
1555
  drawSvCanvas(canvas, this.h);
1686
1556
  canvas._cpLastHue = this.h;
@@ -1722,7 +1592,7 @@ function initializeColorpickerPlugin() {
1722
1592
 
1723
1593
  // ---- Library rendering ----
1724
1594
  //
1725
- // Templates are nested in natural HTML hierarchy (x-for style):
1595
+ // Templates nest as group > palette > swatch:
1726
1596
  // <template x-colorpicker.library>
1727
1597
  // <template x-colorpicker.library-group> <!-- scope: group -->
1728
1598
  // <template x-colorpicker.library-palette> <!-- scope: palette -->
@@ -1732,69 +1602,46 @@ function initializeColorpickerPlugin() {
1732
1602
  // </template>
1733
1603
  // </template>
1734
1604
  //
1735
- // Each inner <template> is replaced in-place by clones (siblings before it, then removed).
1736
- // Data shape after normalization: [{ name?, colors?: Swatch[], palettes?: Palette[] }].
1737
- // If a group has only `colors` (flat, e.g. Recent), it's auto-wrapped as a single
1738
- // unnamed palette so nested templates still work uniformly.
1605
+ // Each inner <template> is replaced in place by clones. Flat groups (Recent)
1606
+ // are auto-wrapped as one unnamed palette so nesting works uniformly.
1739
1607
 
1740
1608
  renderLibrary() {
1741
1609
  if (this._libraryEffectBound) return;
1742
1610
  this._libraryEffectBound = true;
1743
1611
  if (!this.libraryContainers.length) return;
1744
- // All registered containers share the same data source / render key. Use
1745
- // the first for any Alpine scope evaluation that needs an element.
1612
+ // Containers share one data source; use the first for scope eval.
1746
1613
  const evalHost = this.libraryContainers[0];
1747
1614
 
1748
1615
  if (this.libraryRootValue && window.Alpine?.effect && window.Alpine?.evaluateLater) {
1749
- // Explicit expression — bypass discovery, re-render reactively when deps change
1616
+ // Explicit expression — bypass discovery, re-render on dep change.
1750
1617
  const evalFn = Alpine.evaluateLater(evalHost, this.libraryRootValue);
1751
1618
  Alpine.effect(() => {
1752
1619
  evalFn(v => { this._libraryResolvedData = v; this._doRenderLibrary(); });
1753
1620
  });
1754
1621
  } else if (window.Alpine?.effect) {
1755
- // Zero-config — scan `$x.manifest.data` for entries flagged with a
1756
- // `colorpicker:` key (mirroring how `locales:`, `appwriteTableId`, etc.
1757
- // self-identify their plugin). Each flagged entry is loaded normally
1758
- // by the data plugin; we collect the resulting `$x.<name>` values and
1759
- // merge them into the library in declaration order.
1760
- //
1761
- // Multiple sources are supported — split your default-palette overrides
1762
- // (`_tailwind`, `_ios`) and your custom palettes across as many files
1763
- // as you like. Order in `manifest.data` determines render order.
1764
- //
1765
- // Everything happens synchronously inside the effect so Alpine tracks
1766
- // all reactive deps ($locale, $x.manifest, each $x.<name>) and re-runs
1767
- // the full pass on any change. Evaluate against document.body so $x
1768
- // magic is always in scope (the container may be detached/popover
1769
- // content without its own scope chain).
1622
+ // Zero-config — scan $x.manifest.data for `colorpicker:`-flagged
1623
+ // entries, collect their $x.<name> values, merge in declaration
1624
+ // order. Runs synchronously in the effect so Alpine tracks all deps.
1625
+ // Eval against document.body so $x magic is in scope (popover content
1626
+ // may be detached).
1770
1627
  const evalCtx = document.body;
1771
1628
 
1772
- // Manifest's data plugin REPLACES the $x.<source> proxy reference on load
1773
- // rather than mutating in place. Alpine.effect can't catch that change via
1774
- // property tracking, so we pair it with a short-lived poller that RE-READS
1775
- // discovery until the data is loaded. We only actually re-render when the
1776
- // serialized content has changed since the last render — otherwise each
1777
- // poll tick would tear down and rebuild the whole library, thrashing the
1778
- // DOM (and invalidating x-dropdown.context menu id lookups mid-flight).
1629
+ // The data plugin REPLACES the $x.<source> proxy on load rather than
1630
+ // mutating, which Alpine.effect can't catch by property tracking — so a
1631
+ // short poller re-reads until loaded. Re-render only when serialized
1632
+ // content changed (else each tick rebuilds the whole library, thrashing
1633
+ // the DOM and invalidating x-dropdown.context id lookups mid-flight).
1779
1634
  const keyOf = (names, collected) => {
1780
- // Include the Recent list in the key so additions/removals trigger a
1781
- // re-render. Reading _recentStore.list inside the Alpine.effect also
1782
- // establishes reactivity on it, so cookie mutations (pushRecent /
1783
- // removeRecent) fire the effect and change the key.
1635
+ // Recent list is in the key (and read here for reactivity) so
1636
+ // push/removeRecent fire the effect and change the key.
1784
1637
  const recentKey = _recentStore.list.slice(0, _recentMax).join(',');
1785
1638
  try { return recentKey + '#' + names.join('|') + '::' + JSON.stringify(collected); }
1786
1639
  catch { return recentKey + '#' + names.join('|') + '::[unserializable]'; }
1787
1640
  };
1788
1641
  const readSources = () => {
1789
- // Discover palette sources two ways:
1790
- // 1. Preferred: a top-level manifest `"colorpicker"` pointer
1791
- // naming one or more NORMAL data sources ("colors" or
1792
- // ["colors","brand"]). The sources register as plain
1793
- // (optionally localized) data — loading, locale reloads,
1794
- // and `_ui` piggybacking all work untouched.
1795
- // 2. Legacy: a `colorpicker` key flagged on the data entry
1796
- // itself. Kept for back-compat; note the flag-wrapped
1797
- // locale-map shape is NOT loadable by the data plugin.
1642
+ // Two discovery paths: (1) preferred top-level manifest
1643
+ // "colorpicker" pointer naming normal data sources; (2) legacy
1644
+ // `colorpicker` key flagged on the data entry itself (back-compat).
1798
1645
  const names = [];
1799
1646
  try {
1800
1647
  const ptr = Alpine.evaluate(evalCtx, '$x && $x.manifest && $x.manifest.colorpicker');
@@ -1834,8 +1681,7 @@ function initializeColorpickerPlugin() {
1834
1681
  this._libraryDiscoveredKey = key;
1835
1682
  this._libraryDiscoveredData = collected;
1836
1683
  this._doRenderLibrary();
1837
- // A `_ui` source loading late (poller-driven) won't change
1838
- // $locale, so re-localize the rest of the picker here too.
1684
+ // A late-loading `_ui` source won't change $locale — re-localize here.
1839
1685
  this._applyUi(this.rootEl);
1840
1686
  }
1841
1687
  return ready;
@@ -1846,12 +1692,9 @@ function initializeColorpickerPlugin() {
1846
1692
  try { Alpine.evaluate(evalCtx, '$locale && $locale.current'); } catch {}
1847
1693
  try { Alpine.evaluate(evalCtx, '$x && $x.manifest && $x.manifest._loadedFrom'); } catch {}
1848
1694
  runDiscovery();
1849
- // Re-localize the whole picker (tabs + any mounted menus) on
1850
- // locale switch — this effect re-runs when $locale.current changes,
1851
- // and _applyUi re-resolves $x/$locale references inside it.
1695
+ // Re-localize the whole picker on locale switch.
1852
1696
  this._applyUi(this.rootEl);
1853
- // Kick the poller only until data is ready it re-checks every 150ms
1854
- // but skips actual re-render when the content is unchanged.
1697
+ // Poll (150ms) only until data is ready; skips re-render when unchanged.
1855
1698
  if (!this._libraryPollTimer) {
1856
1699
  let attempts = 0;
1857
1700
  this._libraryPollTimer = setInterval(() => {
@@ -1884,17 +1727,13 @@ function initializeColorpickerPlugin() {
1884
1727
  } else {
1885
1728
  groups = composeLibraryFromSources(this._libraryDiscoveredData || []);
1886
1729
  }
1887
- // Resolve any `$x.<path>` or `${...}` template-literal references in
1888
- // group / palette / swatch names, so a single colorpicker file can
1889
- // chain into a separate localization data source without dev-side
1890
- // template tweaks. Reactive reads inside `Alpine.evaluate` register
1891
- // deps on the surrounding render effect → locale switches re-render.
1730
+ // Resolve $x/${...} references in group/palette/swatch names (reactive
1731
+ // reads register deps locale switches re-render).
1892
1732
  return _resolveLibraryRefs(groups);
1893
1733
  },
1894
1734
 
1895
- // Render ONE container (used when a new container registers post-mount
1896
- // e.g. a gradient layer clone's inline library div). Avoids tearing
1897
- // down every other container's x-dropdown.context init timers.
1735
+ // Render ONE container (new post-mount container, e.g. a layer clone's
1736
+ // library div) without tearing down others' x-dropdown.context timers.
1898
1737
  _renderIntoContainer(container) {
1899
1738
  if (!container || !container.isConnected) return;
1900
1739
  const groups = this._resolveLibraryGroups();
@@ -1958,11 +1797,8 @@ function initializeColorpickerPlugin() {
1958
1797
  return this.layersContainer.querySelectorAll(':scope > [data-cp-layer-clone]')[li] || null;
1959
1798
  },
1960
1799
 
1961
- // Resolve the default-menu chrome text (English fallbacks overlaid with
1962
- // any project `_ui` overrides) and stamp it onto a cloned default-template
1963
- // subtree. Called at each clone site and re-run inside the library locale
1964
- // effect, so switching locale (or a late data load) re-renders the labels.
1965
- // Custom dev templates carry no markers → no-op.
1800
+ // Resolve default-menu chrome text (fallbacks + project `_ui` overrides)
1801
+ // and stamp onto a cloned default-template subtree. No-op for custom templates.
1966
1802
  _applyUi(scopeEl) {
1967
1803
  if (!scopeEl || !window.ManifestUI) return;
1968
1804
  let ui;
@@ -1999,24 +1835,15 @@ function initializeColorpickerPlugin() {
1999
1835
  root.setAttribute('data-gradient-type', layer.type);
2000
1836
  root._cpLayerIndex = li;
2001
1837
 
2002
- // Expose the layer's position + type to the clone's Alpine scope so
2003
- // devs can bind classes/attributes reactively. Available in scope:
2004
- // layerType — 'linear' | 'radial' | 'conic'
2005
- // layerIndex — 0-based position of this layer
2006
- // layerCount — total number of layers in the picker
2007
- // Examples:
2008
- // :class="'layer-type-' + layerType"
2009
- // :disabled="layerIndex === 0" (Move Up)
2010
- // :disabled="layerIndex === layerCount - 1" (Move Down)
2011
- // :disabled="layerCount === 1" (Remove)
1838
+ // Expose layerType / layerIndex / layerCount to the clone's Alpine
1839
+ // scope for reactive :class / :disabled bindings.
2012
1840
  root.setAttribute('x-data', '{ '
2013
1841
  + 'layerType: ' + JSON.stringify(layer.type) + ', '
2014
1842
  + 'layerIndex: ' + li + ', '
2015
1843
  + 'layerCount: ' + this.layers.length
2016
1844
  + ' }');
2017
1845
 
2018
- // Uniquify x-dropdown / x-dropdown.context / x-dropdown.hover IDs
2019
- // within this clone so per-layer dropdowns don't collide.
1846
+ // Uniquify x-dropdown IDs so per-layer dropdowns don't collide.
2020
1847
  uniquifyDropdownIdsIn(root, this.pickerUid + '-layer-' + li);
2021
1848
 
2022
1849
  // Render stops bar content
@@ -2036,11 +1863,9 @@ function initializeColorpickerPlugin() {
2036
1863
 
2037
1864
  this.layersContainer.appendChild(root);
2038
1865
 
2039
- // Localize this clone's default menu chrome (gradient types, layer
2040
- // actions, stop actions). No-op for dev-supplied layer templates.
2041
1866
  this._applyUi(root);
2042
1867
 
2043
- // Let Alpine/Manifest process the clone (x-dropdown, x-icon, and nested x-colorpicker directives)
1868
+ // Let Alpine process the clone (x-dropdown, x-icon, nested x-colorpicker).
2044
1869
  if (window.Alpine?.initTree) {
2045
1870
  requestAnimationFrame(() => Alpine.initTree(root));
2046
1871
  }
@@ -2067,7 +1892,7 @@ function initializeColorpickerPlugin() {
2067
1892
  handle.setAttribute('data-cp-stop-handle', '');
2068
1893
  handle.style.left = stop.position + '%';
2069
1894
  handle.style.backgroundColor = colorToRgba(stop.color);
2070
- // .active reflects whether this stop's accordion is currently open
1895
+ // .active = this stop's accordion is open
2071
1896
  if (this.openStop && this.openStop.layerIndex === layerIndex && this.openStop.stopIndex === si) {
2072
1897
  handle.classList.add('active');
2073
1898
  }
@@ -2087,8 +1912,7 @@ function initializeColorpickerPlugin() {
2087
1912
  if (e.button !== 0) return;
2088
1913
  e.stopPropagation();
2089
1914
  self.selectStop(layerIndex, si);
2090
- // .active is set by the re-render after toggleStop;
2091
- // don't preemptively toggle here (would be wrong for drag-without-toggle).
1915
+ // .active is set by the re-render after toggleStop — don't preempt it.
2092
1916
  dragging = true; moved = false; startX = e.clientX;
2093
1917
  cachedBarRect = barEl.getBoundingClientRect();
2094
1918
  handle.setPointerCapture(e.pointerId);
@@ -2099,10 +1923,9 @@ function initializeColorpickerPlugin() {
2099
1923
  if (moved) throttledDrag(e);
2100
1924
  });
2101
1925
  handle.addEventListener('pointerup', () => {
2102
- // Only toggle/cleanup if we had a valid left-click drag session.
2103
- // Right-click never sets `dragging=true` (pointerdown bails for button!==0),
2104
- // so this pointerup would otherwise still call toggleStop() and
2105
- // destroy the layer clone — killing the context menu that just opened.
1926
+ // Only act on a real left-drag session. Right-click never sets
1927
+ // dragging, so bailing here keeps its context menu from being
1928
+ // destroyed by a stray toggleStop() re-render.
2106
1929
  if (!dragging) return;
2107
1930
  const wasMoved = moved;
2108
1931
  dragging = false; moved = false; cachedBarRect = null;
@@ -2119,13 +1942,10 @@ function initializeColorpickerPlugin() {
2119
1942
  containerEl.innerHTML = '';
2120
1943
  const source = this.solidTemplate || _defaultSolidTpl;
2121
1944
  const frag = source.content.cloneNode(true);
2122
- // Uniquify any x-dropdown menu ids inside the cloned solid panel
2123
- // (e.g. the `color-space-menu` from the default template) so two
2124
- // pickers on the same page don't share the same popover element.
1945
+ // Uniquify x-dropdown menu ids so two pickers don't share a popover.
2125
1946
  uniquifyDropdownIdsIn(frag, this.pickerUid + '-solid');
2126
1947
  containerEl.appendChild(frag);
2127
1948
 
2128
- // Localize the default solid panel's chrome (the "Grab color" item).
2129
1949
  this._applyUi(containerEl);
2130
1950
 
2131
1951
  const refs = this._collectSolidRefs(containerEl);
@@ -2139,8 +1959,7 @@ function initializeColorpickerPlugin() {
2139
1959
  return refs;
2140
1960
  },
2141
1961
 
2142
- // Mount the full gradient panel into an instance container.
2143
- // Uses <template x-colorpicker.gradient> if provided, otherwise the default.
1962
+ // Mount the full gradient panel (dev template or default) into a container.
2144
1963
  _mountGradientInstance(containerEl) {
2145
1964
  if (!containerEl) return;
2146
1965
  containerEl.innerHTML = '';
@@ -2148,9 +1967,8 @@ function initializeColorpickerPlugin() {
2148
1967
  const frag = source.content.cloneNode(true);
2149
1968
  containerEl.appendChild(frag);
2150
1969
 
2151
- // Alpine processes the inner x-colorpicker.* directives (add-layer,
2152
- // gradient-layers, layer-options template, set-gradient-value) which
2153
- // register with THIS state via ancestor traversal.
1970
+ // Alpine processes the inner x-colorpicker.* directives, which register
1971
+ // with THIS state via ancestor traversal.
2154
1972
  if (window.Alpine?.initTree) Alpine.initTree(containerEl);
2155
1973
  },
2156
1974
 
@@ -2176,7 +1994,7 @@ function initializeColorpickerPlugin() {
2176
1994
  let dragging = false;
2177
1995
  let cachedRect = null;
2178
1996
  const pick = (e) => {
2179
- // Cache the rect while dragging; invalidated on pointerdown
1997
+ // Rect cached during drag, invalidated on pointerdown.
2180
1998
  if (!cachedRect) cachedRect = refs.canvas.getBoundingClientRect();
2181
1999
  const rect = cachedRect;
2182
2000
  self.s = Math.max(0, Math.min(100, ((e.clientX - rect.left) / rect.width) * 100));
@@ -2190,7 +2008,7 @@ function initializeColorpickerPlugin() {
2190
2008
  cachedRect = refs.canvas.getBoundingClientRect();
2191
2009
  refs.wrapper.setPointerCapture(e.pointerId);
2192
2010
  self._markUserChange(false);
2193
- pick(e); // immediate on first click (not throttled)
2011
+ pick(e); // immediate on first click
2194
2012
  });
2195
2013
  refs.wrapper.addEventListener('pointermove', (e) => { if (dragging) throttledPick(e); });
2196
2014
  refs.wrapper.addEventListener('pointerup', () => { dragging = false; cachedRect = null; });
@@ -2245,8 +2063,7 @@ function initializeColorpickerPlugin() {
2245
2063
  // ---- Picker mount (after all children registered) ----
2246
2064
 
2247
2065
  mount() {
2248
- // Tier 2: if the container has no declared UI (templates are inert overrides),
2249
- // inject the full default UI. Templates alone don't count as "declared UI".
2066
+ // No declared UI (templates alone don't count) inject the default UI.
2250
2067
  const noDeclared = !this.solidTemplate && !this.layerTemplate && !this.gradientTemplate
2251
2068
  && this.solidInstances.length === 0 && this.gradientInstances.length === 0
2252
2069
  && !this.layersContainer && this.gradientValueInputs.length === 0
@@ -2256,23 +2073,16 @@ function initializeColorpickerPlugin() {
2256
2073
  this._injectDefaultUI();
2257
2074
  }
2258
2075
 
2259
- // Auto-popovers register their picker state BEFORE the swatch hook gets
2260
- // a chance to set `x-dropdown` on the trigger element, so the early
2261
- // triggerBtn lookup misses. By mount-time (setTimeout 0) the swatch
2262
- // wiring has finished, so re-query if we don't have one yet.
2076
+ // Auto-popovers register before the swatch hook sets x-dropdown on the
2077
+ // trigger, so the early lookup misses. Re-query now that wiring's done.
2263
2078
  if (!this.triggerBtn && this.rootEl.id) {
2264
2079
  this.triggerBtn = document.querySelector(`[x-dropdown="${this.rootEl.id}"]`);
2265
2080
  }
2266
2081
 
2267
- // Initialize the picker state from whatever source has a real value.
2268
- // Resolution priority matches the retarget flow on swatch-click so the
2269
- // picker's reactive value reads (`$colorpicker(id)`, .hex, .css, etc.)
2270
- // are correct from first paint not just after the user opens the menu.
2271
- // 1. trigger swatch's x-model getter
2272
- // 2. trigger swatch's paired hidden input (form-participation flow)
2273
- // 3. trigger swatch's `value` attribute
2274
- // 4. picker container's own hidden input child
2275
- // 5. fallback '#000000'
2082
+ // Init from the first source with a real value (matches the swatch-click
2083
+ // retarget priority so value reads are correct from first paint):
2084
+ // 1. trigger x-model getter 2. trigger's hidden input
2085
+ // 3. trigger `value` attr 4. container hidden input 5. '#000000'
2276
2086
  let initVal = '';
2277
2087
  const tb = this.triggerBtn;
2278
2088
  if (tb) {
@@ -2285,8 +2095,7 @@ function initializeColorpickerPlugin() {
2285
2095
  if (!initVal && this.hiddenInput) initVal = this.hiddenInput.value;
2286
2096
  this.setFromString(initVal || '#000000');
2287
2097
 
2288
- // Seed trigger swatch CSS var so the border-color derivation paints
2289
- // correctly before any interaction.
2098
+ // Seed trigger swatch CSS var so border-color paints before interaction.
2290
2099
  if (this.triggerBtn) this.triggerBtn.style.setProperty('--color-picker-swatch', this.toSwatchColor());
2291
2100
 
2292
2101
  // Mount all solid-panel instances (Solid tab + any others)
@@ -2297,9 +2106,8 @@ function initializeColorpickerPlugin() {
2297
2106
  }
2298
2107
  this.solidTabRefs = firstSolidRefs;
2299
2108
 
2300
- // Mount all gradient-panel instances (the full gradient panel).
2301
- // This populates them with the gradient template (or default), which in turn
2302
- // registers gradient-layers / layer-options / set-gradient-value with this state.
2109
+ // Mount gradient-panel instances (registers gradient-layers /
2110
+ // layer-options / set-gradient-value with this state).
2303
2111
  for (const inst of this.gradientInstances) this._mountGradientInstance(inst);
2304
2112
 
2305
2113
  // Render gradient layers (only if a container is declared)
@@ -2319,42 +2127,35 @@ function initializeColorpickerPlugin() {
2319
2127
  // Initial sync
2320
2128
  this.syncToInput();
2321
2129
 
2322
- // Render the library — _doRenderLibrary clones the (optional) library template
2323
- // into the container and expands nested group/palette/swatch templates in-place.
2324
2130
  if (this.libraryContainers.length) this.renderLibrary();
2325
2131
 
2326
2132
  // ---- Recent-list commit wiring ----
2327
- // Seed the initial baseline. Popover pickers re-seed on toggle→open.
2133
+ // Seed the baseline; popover pickers re-seed on toggle→open.
2328
2134
  this._startCommitCycle();
2329
2135
 
2330
- // Broad user-interaction detector: any pointerdown or input event inside
2331
- // the picker marks the cycle as "user-touched". This covers all gradient
2332
- // controls (add-layer, set-angle, stop drags, textarea edits, etc.) without
2333
- // having to instrument each handler. Library swatches are detected by
2334
- // scope ancestry so a preset pick is correctly flagged as library-sourced.
2136
+ // Broad user-interaction detector any pointerdown/input marks the cycle
2137
+ // touched (covers all controls). Library picks flagged by scope ancestry.
2335
2138
  this.rootEl.addEventListener('pointerdown', (e) => {
2336
2139
  const fromLibrary = !!e.target.closest('[x-data*="swatch:"]');
2337
2140
  this._markUserChange(fromLibrary);
2338
2141
  });
2339
2142
  this.rootEl.addEventListener('input', () => {
2340
- // 'input' on form controls = user typing / dragging sliders
2341
2143
  this._markUserChange(false);
2342
2144
  });
2343
2145
 
2344
2146
  if (this.rootEl.hasAttribute('popover')) {
2345
- // Popover mode: open/close are the commit boundaries
2147
+ // Popover: open/close are the commit boundaries
2346
2148
  this.rootEl.addEventListener('toggle', (e) => {
2347
2149
  if (e.newState === 'open') this._startCommitCycle();
2348
2150
  if (e.newState === 'closed') {
2349
- // Write to the triggering swatch's model FIRST — _tryCommitRecent
2350
- // resets the user-change flag on success, which would otherwise
2351
- // short-circuit _commitToTrigger.
2151
+ // Commit to trigger FIRST — _tryCommitRecent resets the
2152
+ // user-change flag, which would short-circuit _commitToTrigger.
2352
2153
  this._commitToTrigger();
2353
2154
  this._tryCommitRecent();
2354
2155
  }
2355
2156
  });
2356
2157
  } else {
2357
- // Inline mode: commit when focus leaves the picker entirely
2158
+ // Inline: commit when focus leaves the picker
2358
2159
  this.rootEl.addEventListener('focusout', (e) => {
2359
2160
  const moved = e.relatedTarget;
2360
2161
  if (!moved || !this.rootEl.contains(moved)) {
@@ -2412,17 +2213,15 @@ function initializeColorpickerPlugin() {
2412
2213
  }
2413
2214
  }
2414
2215
 
2415
- // Localize the tab chrome before initTree so x-tooltip caches the
2416
- // resolved label rather than the English default.
2216
+ // Localize before initTree so x-tooltip caches the resolved label.
2417
2217
  this._applyUi(this.rootEl);
2418
2218
 
2419
- // Alpine.initTree fires all x-* directives in the newly injected content,
2420
- // which will register solidTemplate/layerTemplate/solidInstances/layersContainer
2421
- // against THIS state (since ancestor traversal finds this.rootEl).
2219
+ // initTree fires the injected x-* directives, registering the templates
2220
+ // and instances against THIS state via ancestor traversal.
2422
2221
  if (window.Alpine?.initTree) {
2423
2222
  Alpine.initTree(this.rootEl);
2424
2223
  }
2425
- // Stash the auto-injected tab scope wrapper so _syncPickerModeFromTab can read it
2224
+ // Stash the auto-injected tab wrapper for _syncPickerModeFromTab.
2426
2225
  this._autoTabScope = this.rootEl.firstElementChild;
2427
2226
  },
2428
2227
 
@@ -2431,10 +2230,8 @@ function initializeColorpickerPlugin() {
2431
2230
  const autoTab = this._autoTabScope?._x_dataStack?.[0]?.tab;
2432
2231
  const tab = rootTab || autoTab;
2433
2232
  if (!tab) return;
2434
- // Only Solid/Gradient tabs drive the edit mode Library (or any other
2435
- // tab) preserves the current mode. Otherwise switching to Library while
2436
- // editing a gradient would silently demote the picker to solid, breaking
2437
- // the "active" indicator on gradient Recent swatches.
2233
+ // Only Solid/Gradient tabs drive edit mode; Library preserves it (else
2234
+ // opening Library mid-gradient would demote to solid).
2438
2235
  let newMode = null;
2439
2236
  if (tab === 'gradient') newMode = 'gradient';
2440
2237
  else if (tab === 'solid') newMode = 'solid';
@@ -2453,10 +2250,8 @@ function initializeColorpickerPlugin() {
2453
2250
 
2454
2251
  // ---- Public API exposed via $colorpicker magic ----
2455
2252
 
2456
- // Reading `state.snapshot.version` inside each getter registers a reactive
2457
- // dependency. When syncToInput bumps the version, any Alpine effect that
2458
- // read these getters re-runs — and only then do we compute the value.
2459
- // Zero computation if nothing is bound.
2253
+ // track() reads snapshot.version to register a reactive dep; syncToInput
2254
+ // bumps it, re-running bound effects. Zero computation if nothing is bound.
2460
2255
  const track = () => state.snapshot.version;
2461
2256
  state.api = {
2462
2257
  // Reactive reads — lazily computed on demand
@@ -2470,10 +2265,8 @@ function initializeColorpickerPlugin() {
2470
2265
  get format() { track(); return state.format; },
2471
2266
  get pickerMode() { track(); return state.pickerMode; },
2472
2267
 
2473
- // Default string coercion → current CSS value. Lets the developer write
2474
- // :style="`background: ${$colorpicker('id')}`"
2475
- // x-text="$colorpicker('id')"
2476
- // and get the color string directly without picking a specific property.
2268
+ // Default string coercion → current CSS value (e.g. x-text or :style
2269
+ // interpolation of $colorpicker('id') without naming a property).
2477
2270
  [Symbol.toPrimitive]() { track(); return state.toFormattedString(); },
2478
2271
  toString() { track(); return state.toFormattedString(); },
2479
2272
  valueOf() { track(); return state.toFormattedString(); },
@@ -2543,8 +2336,8 @@ function initializeColorpickerPlugin() {
2543
2336
  return name.replace(/\./g, '\\.');
2544
2337
  }
2545
2338
 
2546
- // Rewrite x-dropdown* trigger attributes and the matching <menu id="..."> inside
2547
- // a cloned subtree so multiple clones don't share the same popover ID.
2339
+ // Rewrite x-dropdown* triggers + matching <menu id> in a clone so clones don't
2340
+ // share popover IDs.
2548
2341
  function uniquifyDropdownIdsIn(root, suffix) {
2549
2342
  const attrs = ['x-dropdown', 'x-dropdown.context', 'x-dropdown.hover'];
2550
2343
  for (const attr of attrs) {
@@ -2564,8 +2357,7 @@ function initializeColorpickerPlugin() {
2564
2357
  }
2565
2358
  }
2566
2359
 
2567
- // Coalesce rapid-fire calls (pointermove, input) into at most one per animation frame.
2568
- // The latest args win. Essential for keeping the main thread responsive on busy devices.
2360
+ // Coalesce rapid-fire calls into at most one per animation frame; latest args win.
2569
2361
  function rafThrottle(fn) {
2570
2362
  let scheduled = false;
2571
2363
  let lastArgs;
@@ -2582,9 +2374,8 @@ function initializeColorpickerPlugin() {
2582
2374
  };
2583
2375
  }
2584
2376
 
2585
- // JSON-serialize a value for use in an Alpine x-data attribute via setAttribute.
2586
- // setAttribute does NOT decode HTML entities, so no escaping needed — JSON is
2587
- // already valid JS literal syntax that Alpine can parse directly.
2377
+ // JSON-serialize for an Alpine x-data attribute setAttribute doesn't decode
2378
+ // entities, so JSON is directly parseable, no escaping needed.
2588
2379
  function _jsonStringifyForAlpine(v) {
2589
2380
  try { return JSON.stringify(v); } catch { return '{}'; }
2590
2381
  }
@@ -2625,15 +2416,12 @@ function initializeColorpickerPlugin() {
2625
2416
  Alpine.directive('colorpicker', (el, { modifiers, expression }, { cleanup, evaluateLater }) => {
2626
2417
  // Root: no modifiers
2627
2418
  if (!modifiers || modifiers.length === 0) {
2628
- // <template x-colorpicker> → registered as the page-wide default
2629
- // override. Every bare swatch (`<button x-colorpicker.swatch>`) that
2630
- // would otherwise auto-create an empty popover instead clones from
2631
- // this template. Only one default per page; first declaration wins.
2419
+ // <template x-colorpicker> → page-wide default; bare swatches clone
2420
+ // from it instead of auto-creating an empty popover. First wins.
2632
2421
  if (el.tagName === 'TEMPLATE') {
2633
2422
  if (expression || el.id) {
2634
- // Id-keyed templates are no longer supported — declare the
2635
- // picker inline (`<menu id="X" popover x-colorpicker>`) or
2636
- // wrap it in a Manifest HTML component for reuse.
2423
+ // Id-keyed templates no longer supported — use an inline element
2424
+ // with the same id, or an HTML component.
2637
2425
  try { console.warn('[colorpicker] Id-keyed <template x-colorpicker> is no longer supported. Use a live inline element with the same id, or wrap the picker in an HTML component.'); } catch {}
2638
2426
  return;
2639
2427
  }
@@ -2644,15 +2432,14 @@ function initializeColorpickerPlugin() {
2644
2432
  const state = createPickerState(el);
2645
2433
  el._colorpickerState = state;
2646
2434
 
2647
- // Panel-list expression: `x-colorpicker="['solid', 'gradient']"`.
2648
- // Parsed once here and used by _injectDefaultUI to filter + reorder
2649
- // the default UI's tabs and panels. Anything else is ignored.
2435
+ // Panel-list expression x-colorpicker="['solid','gradient']"
2436
+ // filters + reorders the default UI's tabs/panels.
2650
2437
  state.allowedPanels = parsePanelsExpression(expression);
2651
2438
 
2652
2439
  // Find the form-participation input
2653
2440
  state.hiddenInput = el.querySelector('input[type=color], input[type=hidden]');
2654
2441
 
2655
- // Find trigger button (any button with x-dropdown pointing to this element's ID)
2442
+ // Find trigger button (x-dropdown pointing at this element's ID)
2656
2443
  const id = el.id;
2657
2444
  state.triggerBtn = id ? document.querySelector(`[x-dropdown="${id}"]`) : null;
2658
2445
  if (state.triggerBtn) {
@@ -2661,10 +2448,8 @@ function initializeColorpickerPlugin() {
2661
2448
  });
2662
2449
  }
2663
2450
 
2664
- // Defer mount so all child directives have had a chance to register.
2665
- // setTimeout (rather than rAF) so we still fire when the tab is
2666
- // backgrounded or rAF is throttled — mount must happen for the
2667
- // picker to work, and it doesn't need to be sync'd with paint.
2451
+ // Defer mount so child directives register first. setTimeout (not rAF)
2452
+ // so it still fires when backgrounded.
2668
2453
  setTimeout(() => state.mount(), 0);
2669
2454
 
2670
2455
  cleanup(() => {
@@ -2677,20 +2462,15 @@ function initializeColorpickerPlugin() {
2677
2462
  // Child hook
2678
2463
  const role = modifiers[0];
2679
2464
 
2680
- // Swatches work as triggers OUTSIDE any picker — handle before the ancestor check.
2681
- // We only assign IDs + generate the popover tag; the dropdown plugin (x-dropdown)
2682
- // owns popover mechanics, anchor positioning, and transitions.
2465
+ // Swatches trigger OUTSIDE any picker — handle before the ancestor check.
2466
+ // We assign IDs + the popover tag; x-dropdown owns popover mechanics.
2683
2467
  if (role === 'swatch') {
2684
2468
  if (el._cpSwatchWired) return; // guard against re-firing via initTree
2685
2469
  el._cpSwatchWired = true;
2686
2470
 
2687
2471
  // ---- Optional x-model binding ----
2688
- // When a swatch carries x-model, the expression is the source of truth for
2689
- // that swatch's color. The plugin:
2690
- // • reactively shows the model value as the swatch background (via CSS var)
2691
- // • exposes a read accessor the picker uses on open to load the value
2692
- // • exposes a write accessor the picker uses on close to persist changes
2693
- // The dev can still apply inline style / class overrides on top.
2472
+ // x-model is the swatch's source of truth: reactive background preview,
2473
+ // plus read (open) / write (close) accessors the picker uses.
2694
2474
  const modelExpr = el.getAttribute('x-model');
2695
2475
  if (modelExpr && window.Alpine?.evaluateLater && window.Alpine?.effect) {
2696
2476
  try {
@@ -2705,9 +2485,7 @@ function initializeColorpickerPlugin() {
2705
2485
  });
2706
2486
  });
2707
2487
  el._cpModelGetter = (cb) => readFn(cb);
2708
- // Writer: evaluate `<modelExpr> = <JSON-stringified value>`. JSON.stringify
2709
- // ensures the value is safely serialized (color strings + gradient CSS
2710
- // are all JSON-safe).
2488
+ // Writer: `<modelExpr> = <JSON value>` (color/gradient strings are JSON-safe).
2711
2489
  el._cpModelSetter = (v) => {
2712
2490
  try { Alpine.evaluate(el, `${modelExpr} = ${JSON.stringify(v)}`); } catch {}
2713
2491
  };
@@ -2715,21 +2493,16 @@ function initializeColorpickerPlugin() {
2715
2493
  }
2716
2494
 
2717
2495
  // ---- Initial color via `value` attribute ----
2718
- // The swatch can carry a `value="#abc123"` attribute (mirrors native
2719
- // <input type="color"> semantics). It seeds the picker on first open
2720
- // and the swatch's CSS var so the border-color derivation paints
2721
- // correctly before any interaction.
2496
+ // Mirrors native <input type=color>: seeds the picker on first open and
2497
+ // the swatch CSS var so border-color paints before interaction.
2722
2498
  const valueAttr = el.getAttribute('value');
2723
2499
  if (valueAttr && !el.style.getPropertyValue('--color-picker-swatch')) {
2724
2500
  el.style.setProperty('--color-picker-swatch', valueAttr);
2725
2501
  }
2726
2502
 
2727
2503
  // ---- Form participation via `name` attribute ----
2728
- // When the swatch has `name=`, the plugin synthesizes a sibling
2729
- // <input type="hidden"> with that name (or adopts a matching one
2730
- // already in the DOM). syncToInput then writes the picker's hex
2731
- // value to it, dispatching input/change events for form code.
2732
- // No `name` → no synthesized input — purely decorative swatch.
2504
+ // With `name=`, synthesize (or adopt) a sibling <input type=hidden>;
2505
+ // syncToInput writes the value + dispatches input/change. No name → decorative.
2733
2506
  const nameAttr = el.getAttribute('name');
2734
2507
  if (nameAttr) {
2735
2508
  let hidden = el.parentElement?.querySelector?.(
@@ -2744,8 +2517,7 @@ function initializeColorpickerPlugin() {
2744
2517
  el._cpSynthesizedHidden = hidden;
2745
2518
  }
2746
2519
  el._cpHiddenInput = hidden;
2747
- // Drop `name` from the swatch itself so the form doesn't pick up
2748
- // both the (typically empty) button and the hidden input.
2520
+ // Drop `name` from the button so the form doesn't submit it too.
2749
2521
  if (el.tagName === 'BUTTON') el.removeAttribute('name');
2750
2522
  }
2751
2523
 
@@ -2758,11 +2530,9 @@ function initializeColorpickerPlugin() {
2758
2530
  const wireSwatchTo = (target) => {
2759
2531
  if (!target) return;
2760
2532
 
2761
- // Alias the picker's api under the swatch's id so consumers reading
2762
- // via `$colorpicker('<swatch-id>')` track the right reactive key
2763
- // (otherwise the auto-popover registers under `colorpicker-swatch-N`
2764
- // and the consumer effect's tracked dep on `_pickerRegistry['<swatch-id>']`
2765
- // would never fire). Run after mount so the api exists.
2533
+ // Alias the api under the swatch's id so $colorpicker('<swatch-id>')
2534
+ // tracks the right key (auto-popover otherwise registers under
2535
+ // colorpicker-swatch-N). Runs after mount so the api exists.
2766
2536
  const aliasToSwatchId = () => {
2767
2537
  if (!el.id) return;
2768
2538
  const st = target._colorpickerState
@@ -2774,12 +2544,9 @@ function initializeColorpickerPlugin() {
2774
2544
 
2775
2545
  const isDialog = target.tagName === 'DIALOG';
2776
2546
 
2777
- // For non-dialog popover targets (menu / div with popover), delegate
2778
- // open/close + anchor positioning to the dropdowns plugin so the
2779
- // picker appears anchored to the swatch like a dropdown menu.
2780
- // <dialog> targets are NOT routed through x-dropdown — dialogs are
2781
- // modal/centered surfaces, not anchored to a trigger. We open them
2782
- // imperatively on click via showPopover() or showModal().
2547
+ // Non-dialog popover targets route through x-dropdown (anchored like
2548
+ // a dropdown). <dialog> targets open imperatively on click instead
2549
+ // (modal/centered, not trigger-anchored).
2783
2550
  if (!isDialog
2784
2551
  && target.hasAttribute('popover')
2785
2552
  && !el.hasAttribute('popovertarget')
@@ -2794,6 +2561,7 @@ function initializeColorpickerPlugin() {
2794
2561
  // dialog has a `popover` attribute (light-dismiss); otherwise
2795
2562
  // open as a true modal with backdrop and focus trap.
2796
2563
  if (isDialog) {
2564
+ // Popover-attr dialogs light-dismiss; others open as true modals.
2797
2565
  e.preventDefault();
2798
2566
  try {
2799
2567
  if (target.hasAttribute('popover')) {
@@ -2805,9 +2573,8 @@ function initializeColorpickerPlugin() {
2805
2573
  }
2806
2574
 
2807
2575
  const retarget = () => {
2808
- // Picker state may live on `target` itself (e.g. <menu x-colorpicker>)
2809
- // or on a descendant when the target is a wrapping container such
2810
- // as <dialog> hosting <div x-colorpicker> inside.
2576
+ // State lives on `target` or a descendant (e.g. <dialog> wrapping
2577
+ // <div x-colorpicker>).
2811
2578
  let st = target._colorpickerState;
2812
2579
  if (!st) {
2813
2580
  const inner = target.querySelector?.('[x-colorpicker]');
@@ -2815,17 +2582,10 @@ function initializeColorpickerPlugin() {
2815
2582
  }
2816
2583
  if (!st) { setTimeout(retarget, 0); return; }
2817
2584
  st.triggerBtn = el;
2818
- // Route form-participation writes to this swatch's hidden
2819
- // input (synthesized from `name`, or dev-supplied sibling).
2820
- // Falls back to whatever the picker container already had
2821
- // — preserves the existing inline-input flow.
2585
+ // Route form writes to this swatch's hidden input if it has one.
2822
2586
  if (el._cpHiddenInput) st.hiddenInput = el._cpHiddenInput;
2823
- // Load the trigger's current value. Priority:
2824
- // 1. x-model getter (shared-picker flow)
2825
- // 2. paired hidden input value (form-participation flow)
2826
- // 3. `value` attribute on the swatch
2827
- // 4. --color-picker-swatch CSS var (auto / inline-style flow)
2828
- // 5. fallback '#000000'
2587
+ // Load trigger value by priority: x-model getter, hidden input,
2588
+ // `value` attr, --color-picker-swatch var, then '#000000'.
2829
2589
  if (el._cpModelGetter) {
2830
2590
  el._cpModelGetter(v => { if (typeof v === 'string' && v.length) st.setFromString(v); });
2831
2591
  } else {
@@ -2836,7 +2596,7 @@ function initializeColorpickerPlugin() {
2836
2596
  || '#000000';
2837
2597
  if (current) st.setFromString(current);
2838
2598
  }
2839
- // Defer heavy UI sync so the popover's entry transition runs unimpeded
2599
+ // Defer UI sync so the popover's entry transition runs unimpeded.
2840
2600
  setTimeout(() => st.syncUI(), 0);
2841
2601
  };
2842
2602
  retarget();
@@ -2913,27 +2673,20 @@ function initializeColorpickerPlugin() {
2913
2673
 
2914
2674
  case 'apply-color': {
2915
2675
  el.addEventListener('click', () => {
2916
- // Respect disabled attribute (used when the swatch is a gradient
2917
- // and the user is editing a gradient stop — CSS doesn't allow
2918
- // gradients as color stop values).
2676
+ // Disabled = gradient swatch while editing a stop (can't nest gradients).
2919
2677
  if (el.hasAttribute('disabled')) return;
2920
2678
  const cs = window.getComputedStyle(el);
2921
2679
  const raw = el.style.background || el.style.backgroundColor || cs.backgroundColor;
2922
2680
  if (!raw) return;
2923
- // Library-swatch clicks are marked so the commit cycle knows not
2924
- // to record them as "recent" even if the picker closes afterwards.
2681
+ // Mark library clicks so the commit cycle won't record them as recent.
2925
2682
  const fromLibrary = !!el.closest('[x-data*="swatch:"]');
2926
2683
  const fromStopMenu = !!el.closest('menu[id^="stop-context-menu"]');
2927
2684
  state._markUserChange(fromLibrary);
2928
2685
 
2929
- // Top-level library picks (NOT inside a stop-context-menu) replace
2930
- // the WHOLE field switch picker mode to match the swatch's value
2931
- // type so a solid swatch doesn't become a stop in an existing
2932
- // gradient and a gradient swatch doesn't get parsed as the active
2933
- // stop's color. Stop-menu picks intentionally write to the right-
2934
- // clicked stop and stay in gradient mode.
2935
- // Use _setPickerMode (not _switchToSolidMode) — we don't want to
2936
- // flip the user off whatever tab they're on (typically Library).
2686
+ // Top-level library picks replace the whole field — switch mode to
2687
+ // match the swatch's value type. Stop-menu picks stay in gradient
2688
+ // mode (they target the right-clicked stop). _setPickerMode keeps
2689
+ // the user on their current tab.
2937
2690
  const valueIsGradient = raw.includes('gradient(');
2938
2691
  if (fromLibrary && !fromStopMenu) {
2939
2692
  state._setPickerMode(valueIsGradient ? 'gradient' : 'solid');
@@ -2945,10 +2698,8 @@ function initializeColorpickerPlugin() {
2945
2698
  }
2946
2699
 
2947
2700
  case 'remove-recent': {
2948
- // Expected placement: a menu item inside a <menu popover> referenced by
2949
- // x-dropdown.context on a Recent swatch. The dropdowns plugin stashes the
2950
- // triggering element on `menu._triggerEl`. We read its `data-cp-value`
2951
- // (the raw stored form) and remove that entry from the Recent cookie.
2701
+ // Menu item in a Recent swatch's context menu. The dropdowns plugin
2702
+ // stashes the trigger on menu._triggerEl; read its data-cp-value to remove.
2952
2703
  el.addEventListener('click', () => {
2953
2704
  const menu = el.closest('[popover]');
2954
2705
  const trigger = menu?._triggerEl || menu?._triggerHost;
@@ -2970,9 +2721,7 @@ function initializeColorpickerPlugin() {
2970
2721
  case 'delete-stop':
2971
2722
  case 'set-gradient-type': {
2972
2723
  el.addEventListener('click', () => {
2973
- // Respect :disabled bindings — Alpine toggles the attribute on the
2974
- // element; a disabled menu item shouldn't fire its action.
2975
- if (el.hasAttribute('disabled')) return;
2724
+ if (el.hasAttribute('disabled')) return; // respect :disabled bindings
2976
2725
  const ctx = findLayerContext(el);
2977
2726
  const li = ctx ? ctx.layerIndex : state.activeLayerIndex;
2978
2727
  switch (role) {
@@ -2987,7 +2736,6 @@ function initializeColorpickerPlugin() {
2987
2736
  case 'duplicate-stop': state.duplicateStop(li, state.activeStopIndex); break;
2988
2737
  case 'delete-stop': state.deleteStop(li, state.activeStopIndex); break;
2989
2738
  case 'set-gradient-type': {
2990
- // Value comes from the expression (Alpine parsed) or the attribute
2991
2739
  const type = expression || el.getAttribute('x-colorpicker.set-gradient-type');
2992
2740
  state.setGradientType(li, (type || '').replace(/['"]/g, ''));
2993
2741
  break;
@@ -3044,27 +2792,17 @@ function initializeColorpickerPlugin() {
3044
2792
  });
3045
2793
  return;
3046
2794
 
3047
- // Solid-tab controls (wired via _wireSolidControls when mounted)
3048
- // If the developer places them OUTSIDE a solid-panel instance, wire individually:
2795
+ // Solid-tab controls normally handled inside _mountSolidInstance.
3049
2796
  case 'set-canvas':
3050
2797
  case 'set-hue':
3051
2798
  case 'set-alpha':
3052
2799
  case 'set-alpha-value':
3053
2800
  case 'set-color-value': {
3054
- // These are typically inside a solid-panel template/instance.
3055
- // (The usual path handles them inside _mountSolidInstance.)
3056
2801
  return;
3057
2802
  }
3058
2803
 
3059
- // set-color-space supports two roles:
3060
- // With expression (`<li x-colorpicker.set-color-space="hex">`)
3061
- // click sets that format. Tracked so we can toggle .active on the
3062
- // current choice.
3063
- // • Without expression (`<button x-colorpicker.set-color-space>`) →
3064
- // reactive label whose text reflects the active format. Useful as
3065
- // a dropdown trigger that shows the current format.
3066
- // • <select x-colorpicker.set-color-space> still works through the
3067
- // legacy _wireSolidControls flow inside a solid-panel instance.
2804
+ // set-color-space: with expression = format choice (click sets + tracks
2805
+ // for .active); without = reactive label; <select> uses the legacy flow.
3068
2806
  case 'set-color-space': {
3069
2807
  if (el.tagName === 'SELECT') return; // legacy flow handles it
3070
2808
  const raw = (expression || '').replace(/['"`]/g, '').trim().toLowerCase();
@@ -3087,21 +2825,16 @@ function initializeColorpickerPlugin() {
3087
2825
 
3088
2826
  case 'library': {
3089
2827
  if (el.tagName === 'TEMPLATE') {
3090
- // Dev-defined library layout cloned into the container at render time.
3091
- // Nested <template x-colorpicker.library-group/palette/swatch> are resolved
3092
- // in-place during rendering (x-for style).
2828
+ // Dev-defined library layout, cloned into the container at render time.
3093
2829
  state.libraryTemplate = el;
3094
2830
  } else {
3095
- // Container where the library renders. Multiple containers are supported
3096
- // (e.g., the primary tab AND inline menus like stop-context-menu); each
3097
- // receives an independent clone of the library template. The expression
3098
- // from the FIRST container wins as the data source; others reuse it.
2831
+ // Render container. Multiple are supported (tab + inline stop menus);
2832
+ // each gets its own clone. The first container's expression is the
2833
+ // data source; others reuse it.
3099
2834
  if (!state.libraryContainers.includes(el)) {
3100
2835
  state.libraryContainers.push(el);
3101
- // If the picker is already mounted, render into ONLY this new
3102
- // container so other containers' in-flight directive inits
3103
- // (x-dropdown.context menu lookups, tooltips, etc.) aren't
3104
- // torn down mid-init.
2836
+ // Post-mount: render only this new container so others' in-flight
2837
+ // directive inits aren't torn down mid-init.
3105
2838
  if (state._mounted) state._renderIntoContainer(el);
3106
2839
  }
3107
2840
  if (expression && !state.libraryRootValue) state.libraryRootValue = expression;
@@ -3113,28 +2846,24 @@ function initializeColorpickerPlugin() {
3113
2846
  case 'library-palette':
3114
2847
  case 'library-swatch':
3115
2848
  case 'library-recent-swatch': {
3116
- // Nested templates — no registration. Resolved via querySelector at render time
3117
- // (so their position in the HTML determines where clones land).
3118
- // `library-recent-swatch` is an optional alternate template used only for
3119
- // swatches inside the Recent group (typically wires up x-dropdown.context).
2849
+ // Nested templates — resolved via querySelector at render time, no
2850
+ // registration. library-recent-swatch is an optional Recent-only variant.
3120
2851
  return;
3121
2852
  }
3122
2853
 
3123
2854
  }
3124
2855
  });
3125
2856
 
3126
- // $colorpicker — accessor that is both callable (`$colorpicker('id')`) and property-readable
3127
- // (`$colorpicker.hex` — uses nearest ancestor picker).
2857
+ // $colorpicker — callable ($colorpicker('id')) and property-readable
2858
+ // ($colorpicker.hex, nearest ancestor picker).
3128
2859
  Alpine.magic('colorpicker', (el) => {
3129
2860
  const localState = findAncestorState(el);
3130
2861
  const localApi = localState?.api || null;
3131
2862
 
3132
- // Function form: `$colorpicker('picker-id')` → that picker's API.
3133
- // Reads from the reactive registry so bindings resolve even when the
3134
- // picker is declared later in the DOM than its consumer.
2863
+ // $colorpicker('id') → that picker's API. Reactive registry read so it
2864
+ // resolves even when the picker is declared after its consumer.
3135
2865
  const byId = (id) => {
3136
2866
  if (!id) return localApi;
3137
- // Reactive read: tracks the key even if not yet registered
3138
2867
  const api = _pickerRegistry[id];
3139
2868
  if (api) return api;
3140
2869
  // Allow lookup by swatch button ID (resolve through its popovertarget)
@@ -3149,15 +2878,11 @@ function initializeColorpickerPlugin() {
3149
2878
 
3150
2879
  return new Proxy(byId, {
3151
2880
  get(fn, prop) {
3152
- // Coerce `${$colorpicker}` (no call) to the local picker's CSS string
2881
+ // Coerce `${$colorpicker}` (no call) to the local CSS string.
3153
2882
  if (prop === Symbol.toPrimitive || prop === 'toString' || prop === 'valueOf') {
3154
2883
  return () => (localApi ? localApi.css : '');
3155
2884
  }
3156
- // Global helpers (picker-agnostic) useful for library composition.
3157
- // Each is BOTH callable AND spreadable:
3158
- // {...$colorpicker.tailwind} → default English preset
3159
- // $colorpicker.tailwind(labels) → localized preset (same values, translated names)
3160
- // {...$colorpicker.tailwind(labels)} → spread the localized result
2885
+ // Global preset helpers — each both callable (localized) and spreadable.
3161
2886
  if (prop === 'presets') return _makeCallablePreset(buildDefaultLibrary);
3162
2887
  if (prop === 'tailwind') return _makeCallablePreset(buildTailwindPreset);
3163
2888
  if (prop === 'ios') return _makeCallablePreset(buildIosPreset);
@@ -3175,32 +2900,19 @@ function initializeColorpickerPlugin() {
3175
2900
 
3176
2901
  // ---- Picker resolution: inline / default-template / auto-generated ----
3177
2902
  //
3178
- // Two ways a dev can declare a picker:
3179
- // 1. Live inline element: <menu id="brand-picker" popover x-colorpicker>…</menu>
3180
- // <dialog id="brand-picker" x-colorpicker>…</dialog>
3181
- // <div id="brand-picker" x-colorpicker>…</div>
3182
- // 2. Auto-created (bare): <button x-colorpicker.swatch> generates its own popover.
3183
- // By default, the popover is filled with the plugin's
3184
- // hardcoded fallback UI. Devs can override it page-wide
3185
- // by adding a single `<template x-colorpicker>` (no id)
3186
- // anywhere in the markup — the auto-creator clones from
3187
- // that instead.
3188
- //
3189
- // For "componentize and reuse" use cases that previously needed an id-keyed template,
3190
- // wrap the picker in a Manifest HTML component (`<x-my-picker>`) and drop it wherever
3191
- // it's needed. This keeps the plugin's resolution model deliberately small.
2903
+ // Two ways to declare a picker: (1) a live inline element with x-colorpicker;
2904
+ // (2) a bare <button x-colorpicker.swatch> that auto-creates its own popover
2905
+ // (fallback UI, or a clone of the page-wide <template x-colorpicker> if present).
2906
+ // For reuse, wrap the picker in a Manifest HTML component.
3192
2907
 
3193
2908
  let _defaultColorpickerTemplate = null; // <template x-colorpicker> (no id)
3194
2909
 
3195
2910
  function registerDefaultColorpickerTemplate(tpl) {
3196
- // First wins. Subsequent declarations are ignored — explicit, no surprises.
3197
- if (!tpl || _defaultColorpickerTemplate) return;
2911
+ if (!tpl || _defaultColorpickerTemplate) return; // first wins
3198
2912
  _defaultColorpickerTemplate = tpl;
3199
2913
  }
3200
2914
 
3201
- // Resolve a swatch's target by id. Inline elements only templates are never
3202
- // looked up by id anymore (use a `<menu id="X" x-colorpicker>` or wrap in an
3203
- // HTML component for that pattern).
2915
+ // Resolve a swatch's target by id inline elements only, never templates.
3204
2916
  function resolvePickerById(id) {
3205
2917
  if (!id) return null;
3206
2918
  const live = document.getElementById(id);
@@ -3220,14 +2932,9 @@ function initializeColorpickerPlugin() {
3220
2932
  function createSwatchPopover(customId, panelsExpr) {
3221
2933
  const id = customId || nextAutoSwatchId();
3222
2934
 
3223
- // If a default `<template x-colorpicker>` is registered, clone its root
3224
- // and use that as the swatch's popover. Preserves the dev's chosen
3225
- // wrapper (menu / dialog / div) and any attributes they put on it.
3226
- // The dev's content inside the template is rendered verbatim (mount's
3227
- // noDeclared check sees real children and skips _injectDefaultUI).
3228
- // If the template exists in the DOM but its directive hasn't fired yet
3229
- // (source-order race), scan for it now so swatches earlier in the tree
3230
- // still pick it up.
2935
+ // Clone the default <template x-colorpicker> if registered (preserves the
2936
+ // dev's wrapper + content; mount skips _injectDefaultUI when it sees real
2937
+ // children). Scan for it now in case its directive hasn't fired yet (race).
3231
2938
  if (!_defaultColorpickerTemplate) {
3232
2939
  const candidates = document.querySelectorAll('template[x-colorpicker]');
3233
2940
  for (const t of candidates) {
@@ -3255,8 +2962,7 @@ function initializeColorpickerPlugin() {
3255
2962
  // No default template → empty <menu> populated by _injectDefaultUI on mount.
3256
2963
  const menu = document.createElement('menu');
3257
2964
  menu.setAttribute('popover', '');
3258
- // Pass the panel-list expression through to the root x-colorpicker directive
3259
- // so the auto-created popover only shows the panels the swatch requested.
2965
+ // Pass the panel-list expression through so the popover shows only requested panels.
3260
2966
  menu.setAttribute('x-colorpicker', panelsExpr || '');
3261
2967
  menu.id = id;
3262
2968
  menu.className = 'colorpicker dropdown-menu';
@@ -3274,8 +2980,7 @@ function initializeColorpickerPlugin() {
3274
2980
  const trimmed = expr.trim();
3275
2981
  if (!trimmed.startsWith('[') || !trimmed.endsWith(']')) return null;
3276
2982
  try {
3277
- // JSON.parse after normalizing single quotes the expressions we accept
3278
- // are simple string-array literals, so a quote swap is safe.
2983
+ // Simple string-array literals only, so a quote swap before JSON.parse is safe.
3279
2984
  const arr = JSON.parse(trimmed.replace(/'/g, '"'));
3280
2985
  if (!Array.isArray(arr)) return null;
3281
2986
  const out = arr
@@ -3293,8 +2998,7 @@ function initializeColorpickerPlugin() {
3293
2998
  // Track initialization
3294
2999
  let colorpickerPluginInitialized = false;
3295
3000
 
3296
- // True once Alpine has completed its initial DOM walk. Listener is bound at
3297
- // module load so we never miss the event, whatever the script order.
3001
+ // True once Alpine finished its initial DOM walk; listener bound at module load.
3298
3002
  let colorpickerAlpineHasWalked = false;
3299
3003
  document.addEventListener('alpine:initialized', () => { colorpickerAlpineHasWalked = true; });
3300
3004
 
@@ -3303,11 +3007,8 @@ function ensureColorpickerPluginInitialized() {
3303
3007
  if (!window.Alpine || typeof window.Alpine.directive !== 'function') return;
3304
3008
  colorpickerPluginInitialized = true;
3305
3009
  initializeColorpickerPlugin();
3306
- // Only walk existing [x-colorpicker] subtrees ourselves when Alpine has
3307
- // ALREADY finished its initial walk (i.e. this plugin loaded late).
3308
- // Otherwise the directive is registered during `alpine:init` and Alpine's
3309
- // one boot walk processes every element with all sibling directives present;
3310
- // walking here during boot would drop nested plugin content.
3010
+ // Only walk existing subtrees ourselves when Alpine already finished its boot
3011
+ // walk (late load). Walking during boot would drop nested plugin content.
3311
3012
  if (colorpickerAlpineHasWalked && typeof window.Alpine.initTree === 'function') {
3312
3013
  document.querySelectorAll('[x-colorpicker]').forEach(el => { if (!el.__x) window.Alpine.initTree(el); });
3313
3014
  }