luxen-ui 0.9.2 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/cdn/chunks/picker.js +5 -0
  2. package/cdn/chunks/picker.js.map +1 -0
  3. package/cdn/custom-elements.json +73 -49
  4. package/cdn/elements/avatar/avatar.d.ts +1 -0
  5. package/cdn/elements/avatar/avatar.d.ts.map +1 -1
  6. package/cdn/elements/avatar/avatar.js +1 -1
  7. package/cdn/elements/avatar/avatar.js.map +1 -1
  8. package/cdn/elements/prose-editor/prose-editor.d.ts +39 -11
  9. package/cdn/elements/prose-editor/prose-editor.d.ts.map +1 -1
  10. package/cdn/elements/prose-editor/prose-editor.js +36 -36
  11. package/cdn/elements/prose-editor/prose-editor.js.map +1 -1
  12. package/cdn/elements/tree-item/tree-item.js +1 -1
  13. package/cdn/elements/tree-item/tree-item.js.map +1 -1
  14. package/cdn/standalone.js +1768 -4166
  15. package/cdn/standalone.js.map +1 -1
  16. package/dist/custom-elements.json +73 -49
  17. package/dist/elements/avatar/avatar.css +10 -10
  18. package/dist/elements/avatar/avatar.d.ts +1 -0
  19. package/dist/elements/avatar/avatar.d.ts.map +1 -1
  20. package/dist/elements/avatar/avatar.js +1 -0
  21. package/dist/elements/prose-editor/prose-editor.d.ts +39 -11
  22. package/dist/elements/prose-editor/prose-editor.d.ts.map +1 -1
  23. package/dist/elements/prose-editor/prose-editor.js +114 -38
  24. package/dist/elements/tree-item/tree-item.css +7 -1
  25. package/dist/metadata/avatar.json +5 -0
  26. package/dist/metadata/index.json +14 -1
  27. package/dist/metadata/prose-editor.json +8 -0
  28. package/dist/templates/elements/avatar.md +23 -0
  29. package/dist/templates/elements/prose-editor.md +11 -0
  30. package/package.json +2 -3
  31. package/cdn/chunks/module.js +0 -717
  32. package/cdn/chunks/module.js.map +0 -1
  33. package/cdn/chunks/native.js +0 -2
  34. package/cdn/chunks/native.js.map +0 -1
@@ -11,6 +11,11 @@
11
11
  "description": "",
12
12
  "name": "Avatar",
13
13
  "cssProperties": [
14
+ {
15
+ "description": "Box size (any length). Set it inline (e.g. `style=\"--size: 20px\"`) for an arbitrary size beyond the `size` token scale; the font then follows proportionally. The `size` attribute sets it to the matching token.",
16
+ "name": "--size",
17
+ "default": "40px"
18
+ },
14
19
  {
15
20
  "description": "Background fill color for initials and the default icon.",
16
21
  "name": "--color"
@@ -1144,6 +1149,55 @@
1144
1149
  }
1145
1150
  ]
1146
1151
  },
1152
+ {
1153
+ "kind": "javascript-module",
1154
+ "path": "src/html/elements/carousel-item/carousel-item.ts",
1155
+ "declarations": [
1156
+ {
1157
+ "kind": "class",
1158
+ "description": "A single slide inside an `<l-carousel>`.",
1159
+ "name": "CarouselItem",
1160
+ "cssProperties": [
1161
+ {
1162
+ "description": "Aspect ratio of the slide.",
1163
+ "name": "--aspect-ratio"
1164
+ }
1165
+ ],
1166
+ "members": [],
1167
+ "superclass": {
1168
+ "name": "LuxenElement",
1169
+ "module": "/src/html/shared/luxen-element.js"
1170
+ },
1171
+ "tagName": "l-carousel-item",
1172
+ "customElement": true
1173
+ }
1174
+ ],
1175
+ "exports": [
1176
+ {
1177
+ "kind": "js",
1178
+ "name": "CarouselItem",
1179
+ "declaration": {
1180
+ "name": "CarouselItem",
1181
+ "module": "src/html/elements/carousel-item/carousel-item.ts"
1182
+ }
1183
+ }
1184
+ ]
1185
+ },
1186
+ {
1187
+ "kind": "javascript-module",
1188
+ "path": "src/html/elements/carousel-item/index.ts",
1189
+ "declarations": [],
1190
+ "exports": [
1191
+ {
1192
+ "kind": "js",
1193
+ "name": "*",
1194
+ "declaration": {
1195
+ "name": "*",
1196
+ "module": "src/html/elements/carousel-item/carousel-item.js"
1197
+ }
1198
+ }
1199
+ ]
1200
+ },
1147
1201
  {
1148
1202
  "kind": "javascript-module",
1149
1203
  "path": "src/html/elements/checkbox/checkbox.meta.ts",
@@ -1248,55 +1302,6 @@
1248
1302
  }
1249
1303
  ]
1250
1304
  },
1251
- {
1252
- "kind": "javascript-module",
1253
- "path": "src/html/elements/carousel-item/carousel-item.ts",
1254
- "declarations": [
1255
- {
1256
- "kind": "class",
1257
- "description": "A single slide inside an `<l-carousel>`.",
1258
- "name": "CarouselItem",
1259
- "cssProperties": [
1260
- {
1261
- "description": "Aspect ratio of the slide.",
1262
- "name": "--aspect-ratio"
1263
- }
1264
- ],
1265
- "members": [],
1266
- "superclass": {
1267
- "name": "LuxenElement",
1268
- "module": "/src/html/shared/luxen-element.js"
1269
- },
1270
- "tagName": "l-carousel-item",
1271
- "customElement": true
1272
- }
1273
- ],
1274
- "exports": [
1275
- {
1276
- "kind": "js",
1277
- "name": "CarouselItem",
1278
- "declaration": {
1279
- "name": "CarouselItem",
1280
- "module": "src/html/elements/carousel-item/carousel-item.ts"
1281
- }
1282
- }
1283
- ]
1284
- },
1285
- {
1286
- "kind": "javascript-module",
1287
- "path": "src/html/elements/carousel-item/index.ts",
1288
- "declarations": [],
1289
- "exports": [
1290
- {
1291
- "kind": "js",
1292
- "name": "*",
1293
- "declaration": {
1294
- "name": "*",
1295
- "module": "src/html/elements/carousel-item/carousel-item.js"
1296
- }
1297
- }
1298
- ]
1299
- },
1300
1305
  {
1301
1306
  "kind": "javascript-module",
1302
1307
  "path": "src/html/elements/close-button/close-button.meta.ts",
@@ -3803,6 +3808,16 @@
3803
3808
  "description": "Placeholder shown when the editor is empty.",
3804
3809
  "attribute": "placeholder"
3805
3810
  },
3811
+ {
3812
+ "kind": "field",
3813
+ "name": "emojiDataSource",
3814
+ "type": {
3815
+ "text": "string"
3816
+ },
3817
+ "default": "''",
3818
+ "description": "URL the emoji picker fetches its data from. Point this at a locally served\n`emojibase-data` JSON to run fully offline (no CDN). Defaults to the\npicker's bundled CDN source.",
3819
+ "attribute": "emoji-data-source"
3820
+ },
3806
3821
  {
3807
3822
  "kind": "field",
3808
3823
  "name": "validationTarget",
@@ -3997,6 +4012,15 @@
3997
4012
  "default": "''",
3998
4013
  "description": "Placeholder shown when the editor is empty.",
3999
4014
  "fieldName": "placeholder"
4015
+ },
4016
+ {
4017
+ "name": "emoji-data-source",
4018
+ "type": {
4019
+ "text": "string"
4020
+ },
4021
+ "default": "''",
4022
+ "description": "URL the emoji picker fetches its data from. Point this at a locally served\n`emojibase-data` JSON to run fully offline (no CDN). Defaults to the\npicker's bundled CDN source.",
4023
+ "fieldName": "emojiDataSource"
4000
4024
  }
4001
4025
  ],
4002
4026
  "superclass": {
@@ -1,13 +1,13 @@
1
1
  :host {
2
- --_size: 40px;
3
- --_font-size: 1rem;
2
+ --size: 40px;
3
+ --_font-size: calc(var(--size) * 0.4);
4
4
  display: inline-flex;
5
5
  align-items: center;
6
6
  justify-content: center;
7
7
  position: relative;
8
- width: var(--_size);
9
- height: var(--_size);
10
- border-radius: calc(var(--_size) * 0.15);
8
+ width: var(--size);
9
+ height: var(--size);
10
+ border-radius: calc(var(--size) * 0.15);
11
11
  isolation: isolate;
12
12
  vertical-align: middle;
13
13
  flex-shrink: 0;
@@ -15,19 +15,19 @@
15
15
  }
16
16
 
17
17
  :host([size='xs']) {
18
- --_size: 24px;
18
+ --size: 24px;
19
19
  --_font-size: 0.75rem;
20
20
  }
21
21
  :host([size='sm']) {
22
- --_size: 32px;
22
+ --size: 32px;
23
23
  --_font-size: 0.875rem;
24
24
  }
25
25
  :host([size='lg']) {
26
- --_size: 48px;
26
+ --size: 48px;
27
27
  --_font-size: 1.25rem;
28
28
  }
29
29
  :host([size='xl']) {
30
- --_size: 56px;
30
+ --size: 56px;
31
31
  --_font-size: 1.25rem;
32
32
  }
33
33
 
@@ -91,7 +91,7 @@ img {
91
91
  width: 100%;
92
92
  height: 100%;
93
93
  border: none;
94
- border-radius: calc(var(--_size) * 0.15);
94
+ border-radius: calc(var(--size) * 0.15);
95
95
  background-color: var(--color, var(--l-color-bg-fill-neutral-soft));
96
96
  /*
97
97
  * Text over --color, decided by the background's own luminance — never the
@@ -3,6 +3,7 @@ import { LuxenElement } from '../../shared/luxen-element.js';
3
3
  * @summary An avatar component for displaying user images, initials, or a default icon.
4
4
  * @customElement l-avatar
5
5
  *
6
+ * @cssproperty [--size=40px] - Box size (any length). Set it inline (e.g. `style="--size: 20px"`) for an arbitrary size beyond the `size` token scale; the font then follows proportionally. The `size` attribute sets it to the matching token.
6
7
  * @cssproperty --color - Background fill color for initials and the default icon.
7
8
  * @cssproperty --text-color - Initials/icon color over `--color`. Defaults to an auto-derived readable color; set it to enforce a specific brand color (overrides the automatic choice).
8
9
  * @cssproperty --appearance - Set to `circle` for a fully circular avatar (default is a rounded square).
@@ -1 +1 @@
1
- {"version":3,"file":"avatar.d.ts","sourceRoot":"","sources":["../../../src/html/elements/avatar/avatar.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,EAAE,MAAM,+BAA+B,CAAC;AAqB7D;;;;;;;;;;;GAWG;AACH,qBAAa,MAAO,SAAQ,YAAY;IACtC,MAAM,CAAC,MAAM,4BAAwB;IAErC,qFAAqF;IAErF,GAAG,SAAM;IAET,6EAA6E;IAE7E,IAAI,SAAM;IAEV,8DAA8D;IAE9D,IAAI,SAAQ;IAEZ,4DAA4D;IAE5D,KAAK,SAAK;IAEV,+DAA+D;IAE/D,WAAW,UAAS;IAEX,OAAO,CAAC,SAAS,CAAS;IAEnC,UAAU,CAAC,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC;IAMxC,iBAAiB;IAOjB,OAAO;IASP,OAAO,KAAK,IAAI,GAEf;IAED,MAAM;IA6BN,OAAO,CAAC,QAAQ,CAEd;CACH"}
1
+ {"version":3,"file":"avatar.d.ts","sourceRoot":"","sources":["../../../src/html/elements/avatar/avatar.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,EAAE,MAAM,+BAA+B,CAAC;AAqB7D;;;;;;;;;;;;GAYG;AACH,qBAAa,MAAO,SAAQ,YAAY;IACtC,MAAM,CAAC,MAAM,4BAAwB;IAErC,qFAAqF;IAErF,GAAG,SAAM;IAET,6EAA6E;IAE7E,IAAI,SAAM;IAEV,8DAA8D;IAE9D,IAAI,SAAQ;IAEZ,4DAA4D;IAE5D,KAAK,SAAK;IAEV,+DAA+D;IAE/D,WAAW,UAAS;IAEX,OAAO,CAAC,SAAS,CAAS;IAEnC,UAAU,CAAC,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC;IAMxC,iBAAiB;IAOjB,OAAO;IASP,OAAO,KAAK,IAAI,GAEf;IAED,MAAM;IA6BN,OAAO,CAAC,QAAQ,CAEd;CACH"}
@@ -26,6 +26,7 @@ const defaultIcon = svg `<svg class="icon" viewBox="0 0 24 24" fill="currentColo
26
26
  * @summary An avatar component for displaying user images, initials, or a default icon.
27
27
  * @customElement l-avatar
28
28
  *
29
+ * @cssproperty [--size=40px] - Box size (any length). Set it inline (e.g. `style="--size: 20px"`) for an arbitrary size beyond the `size` token scale; the font then follows proportionally. The `size` attribute sets it to the matching token.
29
30
  * @cssproperty --color - Background fill color for initials and the default icon.
30
31
  * @cssproperty --text-color - Initials/icon color over `--color`. Defaults to an auto-derived readable color; set it to enforce a specific brand color (overrides the automatic choice).
31
32
  * @cssproperty --appearance - Set to `circle` for a fully circular avatar (default is a rounded square).
@@ -58,10 +58,17 @@ export declare class ProseEditor extends LuxenFormAssociatedElement {
58
58
  accessor autofocus: boolean;
59
59
  /** Placeholder shown when the editor is empty. */
60
60
  accessor placeholder: string;
61
+ /**
62
+ * URL the emoji picker fetches its data from. Point this at a locally served
63
+ * `emojibase-data` JSON to run fully offline (no CDN). Defaults to the
64
+ * picker's bundled CDN source.
65
+ */
66
+ accessor emojiDataSource: string;
61
67
  private _editorRoot?;
62
68
  private _emojiPicker?;
63
69
  private _emojiPickerPromise?;
64
70
  private _emojiOpenAtPointerDown;
71
+ private _emojiAutoUpdateCleanup?;
65
72
  get validationTarget(): HTMLElement | undefined;
66
73
  private get _toolbar();
67
74
  firstUpdated(): void;
@@ -105,23 +112,34 @@ export declare class ProseEditor extends LuxenFormAssociatedElement {
105
112
  private _onFocus;
106
113
  private _onKeyDown;
107
114
  /**
108
- * The picker is a `popover="auto"`, so the platform owns outside-click
109
- * dismissal: native light-dismiss in the top layer, which survives
110
- * `stopPropagation()` from an ancestor (modal `<l-dialog>`, ProseMirror, Vue
111
- * delegation), unlike emoji-mart's `document` click listener. (`Escape` is
112
- * handled by `_onKeyDown` instead — ProseMirror `preventDefault()`s it, which
113
- * would cancel the native close.)
115
+ * Capture-phase outside-click dismissal the robust fallback the platform's
116
+ * native light-dismiss can't be trusted to provide (see `firstUpdated`).
117
+ * Dismiss when the picker is open and the pointer landed on neither the
118
+ * picker nor the emoji toolbar button (re-clicking the button is handled by
119
+ * the toggle in `_onEmojiButtonClick`). `composedPath()` crosses shadow
120
+ * boundaries, so a click inside the picker's shadow tree still resolves to
121
+ * the host element here.
122
+ */
123
+ private _onDocumentPointerDown;
124
+ /**
125
+ * The picker is a `popover="auto"` parented into the top layer; outside-click
126
+ * dismissal is owned by `_onDocumentPointerDown` (capture phase), which
127
+ * survives `stopPropagation()`/`preventDefault()` from an ancestor (modal
128
+ * `<l-dialog>`, ProseMirror, Vue delegation). (`Escape` is handled by
129
+ * `_onKeyDown` instead — ProseMirror `preventDefault()`s it, which would
130
+ * cancel the native close.)
114
131
  *
115
132
  * We can't wire the button as a native popover invoker (`popoverTargetElement`
116
133
  * / `popovertarget`) because the picker is parented into another shadow tree
117
134
  * (the open `<dialog>` — see `_topLayerContainer`) while the button lives in
118
135
  * this element's shadow root; a cross-tree invoker reference doesn't resolve.
119
136
  * So the toggle is hand-rolled here. The catch: a pointer click on the button
120
- * is "outside" the popover, so native light-dismiss has already closed an open
121
- * picker by the time this `click` fires (light-dismiss runs on `pointerup`).
122
- * We therefore read the open state captured at `pointerdown` to know whether
123
- * this click should re-open or stay closed. Keyboard activation (`detail === 0`)
124
- * has no pointer light-dismiss, so it reads the live state and toggles directly.
137
+ * is "outside" the popover, so native light-dismiss (where it works) has
138
+ * already closed an open picker by the time this `click` fires (light-dismiss
139
+ * runs on `pointerup`). We therefore read the open state captured at
140
+ * `pointerdown` to know whether this click should re-open or stay closed.
141
+ * Keyboard activation (`detail === 0`) has no pointer light-dismiss, so it
142
+ * reads the live state and toggles directly.
125
143
  */
126
144
  private _onEmojiButtonPointerDown;
127
145
  private _onEmojiButtonClick;
@@ -135,6 +153,16 @@ export declare class ProseEditor extends LuxenFormAssociatedElement {
135
153
  */
136
154
  private _topLayerContainer;
137
155
  private get _emojiButton();
156
+ /**
157
+ * Keep the picker pinned to the emoji button while it is open. Floating UI's
158
+ * `autoUpdate` re-runs `_positionEmojiPicker` on scroll (including ancestor
159
+ * scroll), resize, and layout shifts, so the picker stays anchored to the
160
+ * button — the cross-tree equivalent of CSS anchor positioning, which can't be
161
+ * used here because the button lives in this element's shadow root while the
162
+ * picker is parented to the top-layer container (a different tree scope).
163
+ */
164
+ private _startEmojiAutoUpdate;
165
+ private _stopEmojiAutoUpdate;
138
166
  private _positionEmojiPicker;
139
167
  private _renderButton;
140
168
  private _renderToolbarItem;
@@ -1 +1 @@
1
- {"version":3,"file":"prose-editor.d.ts","sourceRoot":"","sources":["../../../src/html/elements/prose-editor/prose-editor.ts"],"names":[],"mappings":"AAAA,OAAO,EAAa,KAAK,cAAc,EAAE,KAAK,cAAc,EAAE,MAAM,KAAK,CAAC;AAO1E,OAAO,EAAE,MAAM,EAAE,KAAK,WAAW,EAAE,MAAM,cAAc,CAAC;AAIxD,OAAO,EAAE,0BAA0B,EAAE,MAAM,+CAA+C,CAAC;AAE3F,OAAO,kBAAkB,CAAC;AAK1B,KAAK,kBAAkB,GACnB,WAAW,GACX,WAAW,GACX,WAAW,GACX,MAAM,GACN,QAAQ,GACR,WAAW,GACX,QAAQ,GACR,WAAW,GACX,YAAY,GACZ,aAAa,GACb,YAAY,GACZ,YAAY,GACZ,iBAAiB,GACjB,MAAM,GACN,OAAO,GACP,YAAY,GACZ,MAAM,GACN,MAAM,GACN,SAAS,CAAC;AA2Bd;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AACH,qBAAa,WAAY,SAAQ,0BAA0B;IACzD,OAAgB,MAAM,4BAAwB;IAE9C,oEAAoE;IACpE,MAAM,EAAG,MAAM,CAAC;IAEhB,4BAA4B;IAE5B,QAAQ,CAAC,WAAW,SAAM;IAE1B,+DAA+D;IAE/D,QAAQ,CAAC,WAAW,SAAM;IAE1B,mGAAmG;IAEnG,QAAQ,CAAC,WAAW,SAAW;IAE/B,8EAA8E;IAO9E,QAAQ,CAAC,OAAO,EAAE,kBAAkB,EAAE,CAAM;IAE5C,8DAA8D;IAE9D,QAAQ,CAAC,aAAa,EAAE,SAAS,GAAG,SAAS,CAAa;IAE1D,sDAAsD;IAEtD,QAAQ,CAAC,gBAAgB,EAAE,KAAK,GAAG,QAAQ,CAAS;IAEpD,oCAAoC;IAEpC,QAAQ,CAAC,SAAS,UAAS;IAE3B,kDAAkD;IAElD,QAAQ,CAAC,WAAW,SAAM;IAE1B,OAAO,CAAC,WAAW,CAAC,CAAiB;IACrC,OAAO,CAAC,YAAY,CAAC,CAAc;IACnC,OAAO,CAAC,mBAAmB,CAAC,CAAuB;IACnD,OAAO,CAAC,uBAAuB,CAAS;IAExC,IAAa,gBAAgB,IAAI,WAAW,GAAG,SAAS,CAEvD;IAED,OAAO,KAAK,QAAQ,GAEnB;IAEQ,YAAY;IAkCZ,OAAO,CAAC,OAAO,EAAE,cAAc,CAAC,IAAI,CAAC;IAMrC,oBAAoB;IAW7B,mFAAmF;IACnF,OAAO,IAAI,MAAM;IAMjB,mDAAmD;IACnD,OAAO,IAAI,WAAW;IAItB,0BAA0B;IAC1B,KAAK;IAII,KAAK;IAIL,IAAI;IAIb,UAAU;IAIV,YAAY;IAIZ,eAAe;IAIf,YAAY;IAIZ,eAAe;IAIf,aAAa,CAAC,KAAK,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC;IAI9B,gBAAgB;IAIhB,iBAAiB;IAIjB,gBAAgB;IAIhB,eAAe;IAIf,iBAAiB;IAIjB,IAAI;IAIJ,IAAI;IAIJ,UAAU;IAaD,iBAAiB;IAK1B,mFAAmF;IACnF,OAAO,CAAC,UAAU;IAalB,OAAO,CAAC,WAAW;IAQnB,OAAO,CAAC,eAAe;IAQvB;;;;;;OAMG;IACH,OAAO,CAAC,iBAAiB;IAQzB,OAAO,CAAC,QAAQ,CAId;IAEF,OAAO,CAAC,UAAU,CAMhB;IAIF;;;;;;;;;;;;;;;;;;OAkBG;IACH,OAAO,CAAC,yBAAyB,CAE/B;YAEY,mBAAmB;IAWjC,OAAO,CAAC,kBAAkB;IA0C1B;;;;;;OAMG;IACH,OAAO,CAAC,kBAAkB;IAe1B,OAAO,KAAK,YAAY,GAEvB;YAEa,oBAAoB;IAiBlC,OAAO,CAAC,aAAa;IA0BrB,OAAO,CAAC,kBAAkB;IA0IjB,MAAM;CAyBhB"}
1
+ {"version":3,"file":"prose-editor.d.ts","sourceRoot":"","sources":["../../../src/html/elements/prose-editor/prose-editor.ts"],"names":[],"mappings":"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"}
@@ -15,14 +15,14 @@ var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (
15
15
  if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
16
16
  return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
17
17
  };
18
- var _ProseEditor_initialHtml_accessor_storage, _ProseEditor_initialJson_accessor_storage, _ProseEditor_editorClass_accessor_storage, _ProseEditor_toolbar_accessor_storage, _ProseEditor_toolbarPreset_accessor_storage, _ProseEditor_toolbarPlacement_accessor_storage, _ProseEditor_autofocus_accessor_storage, _ProseEditor_placeholder_accessor_storage;
18
+ var _ProseEditor_initialHtml_accessor_storage, _ProseEditor_initialJson_accessor_storage, _ProseEditor_editorClass_accessor_storage, _ProseEditor_toolbar_accessor_storage, _ProseEditor_toolbarPreset_accessor_storage, _ProseEditor_toolbarPlacement_accessor_storage, _ProseEditor_autofocus_accessor_storage, _ProseEditor_placeholder_accessor_storage, _ProseEditor_emojiDataSource_accessor_storage;
19
19
  import { unsafeCSS } from 'lit';
20
20
  import { html } from 'lit/static-html.js';
21
21
  import { staticTag } from '../../static-tag.js';
22
22
  import { property } from 'lit/decorators.js';
23
23
  import { classMap } from 'lit/directives/class-map.js';
24
24
  import { map } from 'lit/directives/map.js';
25
- import { computePosition, flip, offset, shift } from '@floating-ui/dom';
25
+ import { autoUpdate, computePosition, flip, offset, shift } from '@floating-ui/dom';
26
26
  import { Editor } from '@tiptap/core';
27
27
  import StarterKit from '@tiptap/starter-kit';
28
28
  import Highlight from '@tiptap/extension-highlight';
@@ -102,6 +102,7 @@ export class ProseEditor extends LuxenFormAssociatedElement {
102
102
  _ProseEditor_toolbarPlacement_accessor_storage.set(this, 'top');
103
103
  _ProseEditor_autofocus_accessor_storage.set(this, false);
104
104
  _ProseEditor_placeholder_accessor_storage.set(this, '');
105
+ _ProseEditor_emojiDataSource_accessor_storage.set(this, '');
105
106
  this._emojiOpenAtPointerDown = false;
106
107
  this._onFocus = () => {
107
108
  if (!this.disabled && document.activeElement !== this.editor?.view.dom) {
@@ -116,25 +117,44 @@ export class ProseEditor extends LuxenFormAssociatedElement {
116
117
  event.preventDefault();
117
118
  event.stopPropagation();
118
119
  };
120
+ /**
121
+ * Capture-phase outside-click dismissal — the robust fallback the platform's
122
+ * native light-dismiss can't be trusted to provide (see `firstUpdated`).
123
+ * Dismiss when the picker is open and the pointer landed on neither the
124
+ * picker nor the emoji toolbar button (re-clicking the button is handled by
125
+ * the toggle in `_onEmojiButtonClick`). `composedPath()` crosses shadow
126
+ * boundaries, so a click inside the picker's shadow tree still resolves to
127
+ * the host element here.
128
+ */
129
+ this._onDocumentPointerDown = (event) => {
130
+ const picker = this._emojiPicker;
131
+ if (!picker?.matches(':popover-open'))
132
+ return;
133
+ const path = event.composedPath();
134
+ if (path.includes(picker) || (this._emojiButton && path.includes(this._emojiButton)))
135
+ return;
136
+ picker.hidePopover();
137
+ };
119
138
  // --- Emoji picker (lazy-loaded) ---
120
139
  /**
121
- * The picker is a `popover="auto"`, so the platform owns outside-click
122
- * dismissal: native light-dismiss in the top layer, which survives
123
- * `stopPropagation()` from an ancestor (modal `<l-dialog>`, ProseMirror, Vue
124
- * delegation), unlike emoji-mart's `document` click listener. (`Escape` is
125
- * handled by `_onKeyDown` instead — ProseMirror `preventDefault()`s it, which
126
- * would cancel the native close.)
140
+ * The picker is a `popover="auto"` parented into the top layer; outside-click
141
+ * dismissal is owned by `_onDocumentPointerDown` (capture phase), which
142
+ * survives `stopPropagation()`/`preventDefault()` from an ancestor (modal
143
+ * `<l-dialog>`, ProseMirror, Vue delegation). (`Escape` is handled by
144
+ * `_onKeyDown` instead — ProseMirror `preventDefault()`s it, which would
145
+ * cancel the native close.)
127
146
  *
128
147
  * We can't wire the button as a native popover invoker (`popoverTargetElement`
129
148
  * / `popovertarget`) because the picker is parented into another shadow tree
130
149
  * (the open `<dialog>` — see `_topLayerContainer`) while the button lives in
131
150
  * this element's shadow root; a cross-tree invoker reference doesn't resolve.
132
151
  * So the toggle is hand-rolled here. The catch: a pointer click on the button
133
- * is "outside" the popover, so native light-dismiss has already closed an open
134
- * picker by the time this `click` fires (light-dismiss runs on `pointerup`).
135
- * We therefore read the open state captured at `pointerdown` to know whether
136
- * this click should re-open or stay closed. Keyboard activation (`detail === 0`)
137
- * has no pointer light-dismiss, so it reads the live state and toggles directly.
152
+ * is "outside" the popover, so native light-dismiss (where it works) has
153
+ * already closed an open picker by the time this `click` fires (light-dismiss
154
+ * runs on `pointerup`). We therefore read the open state captured at
155
+ * `pointerdown` to know whether this click should re-open or stay closed.
156
+ * Keyboard activation (`detail === 0`) has no pointer light-dismiss, so it
157
+ * reads the live state and toggles directly.
138
158
  */
139
159
  this._onEmojiButtonPointerDown = () => {
140
160
  this._emojiOpenAtPointerDown = !!this._emojiPicker?.matches(':popover-open');
@@ -164,6 +184,13 @@ export class ProseEditor extends LuxenFormAssociatedElement {
164
184
  /** Placeholder shown when the editor is empty. */
165
185
  get placeholder() { return __classPrivateFieldGet(this, _ProseEditor_placeholder_accessor_storage, "f"); }
166
186
  set placeholder(value) { __classPrivateFieldSet(this, _ProseEditor_placeholder_accessor_storage, value, "f"); }
187
+ /**
188
+ * URL the emoji picker fetches its data from. Point this at a locally served
189
+ * `emojibase-data` JSON to run fully offline (no CDN). Defaults to the
190
+ * picker's bundled CDN source.
191
+ */
192
+ get emojiDataSource() { return __classPrivateFieldGet(this, _ProseEditor_emojiDataSource_accessor_storage, "f"); }
193
+ set emojiDataSource(value) { __classPrivateFieldSet(this, _ProseEditor_emojiDataSource_accessor_storage, value, "f"); }
167
194
  get validationTarget() {
168
195
  return this._editorRoot;
169
196
  }
@@ -199,6 +226,15 @@ export class ProseEditor extends LuxenFormAssociatedElement {
199
226
  // `stopPropagation()`, and only needs the event to propagate — not its
200
227
  // default action.
201
228
  document.addEventListener('keydown', this._onKeyDown, true);
229
+ // Outside-click dismissal does NOT rely on native popover light-dismiss:
230
+ // at least one shipping Chromium skips light-dismiss when the underlying
231
+ // `pointerdown`'s default action is prevented — which ProseMirror does for
232
+ // every click inside the editor (it drives its own selection). A
233
+ // capture-phase `document` listener runs before any descendant
234
+ // `preventDefault()`/`stopPropagation()` and doesn't depend on the default
235
+ // action, so it dismisses on every browser, real or synthetic input, modal
236
+ // or page. Native light-dismiss then becomes a redundant enhancement.
237
+ document.addEventListener('pointerdown', this._onDocumentPointerDown, true);
202
238
  this._syncValue();
203
239
  this.requestUpdate();
204
240
  }
@@ -211,6 +247,8 @@ export class ProseEditor extends LuxenFormAssociatedElement {
211
247
  super.disconnectedCallback();
212
248
  this.removeEventListener('focus', this._onFocus);
213
249
  document.removeEventListener('keydown', this._onKeyDown, true);
250
+ document.removeEventListener('pointerdown', this._onDocumentPointerDown, true);
251
+ this._stopEmojiAutoUpdate();
214
252
  this.editor?.destroy();
215
253
  this._editorRoot?.remove();
216
254
  this._emojiPicker?.remove();
@@ -347,28 +385,31 @@ export class ProseEditor extends LuxenFormAssociatedElement {
347
385
  if (this._emojiPicker)
348
386
  return Promise.resolve(this._emojiPicker);
349
387
  return (this._emojiPickerPromise ?? (this._emojiPickerPromise = (async () => {
350
- const [{ Picker }, { default: data }] = await Promise.all([
351
- import('emoji-mart'),
352
- import('@emoji-mart/data'),
353
- ]);
354
- const dark = window.matchMedia('(prefers-color-scheme: dark)').matches;
355
- const picker = new Picker({
356
- parent: this._topLayerContainer(),
357
- data,
358
- theme: dark ? 'dark' : 'light',
359
- onEmojiSelect: ({ native }) => {
360
- this.editor.chain().focus().insertContent(native).run();
361
- picker.hidePopover();
362
- },
388
+ // `emoji-picker-element` is a framework-agnostic web component with no
389
+ // `document`-level click listener of its own to fight the platform.
390
+ // Importing it registers <emoji-picker> and gives us the constructor.
391
+ const { default: Picker } = await import('emoji-picker-element/picker.js');
392
+ const picker = new Picker(this.emojiDataSource ? { dataSource: this.emojiDataSource } : undefined);
393
+ // Theme follows the system color scheme automatically (the element ships
394
+ // a `prefers-color-scheme` media query); a `.light`/`.dark` class would
395
+ // only be needed to override that.
396
+ picker.addEventListener('emoji-click', (event) => {
397
+ const unicode = event.detail.unicode;
398
+ if (!unicode)
399
+ return;
400
+ this.editor.chain().focus().insertContent(unicode).run();
401
+ picker.hidePopover();
363
402
  });
403
+ this._topLayerContainer().append(picker);
364
404
  // A popover="auto" promotes the picker into the top-layer (so it paints
365
- // above a modal <l-dialog> and escapes ancestor overflow clipping) and
366
- // hands outside-click dismissal to the platform's native light-dismiss.
367
- // Top-layer alone isn't enough inside a modal: showModal()
368
- // makes everything outside the dialog's flat-tree subtree inert (visible
369
- // but unclickable), so the picker is parented to the nearest open <dialog>
370
- // see _topLayerContainer. Neutralize the UA popover box; Floating UI
371
- // drives the position in _positionEmojiPicker.
405
+ // above a modal <l-dialog> and escapes ancestor overflow clipping). Outside
406
+ // -click dismissal is driven by `_onDocumentPointerDown`, with native
407
+ // light-dismiss as a redundant enhancement. Top-layer alone isn't enough
408
+ // inside a modal: showModal() makes everything outside the dialog's
409
+ // flat-tree subtree inert (visible but unclickable), so the picker is
410
+ // parented to the nearest open <dialog> see _topLayerContainer.
411
+ // Neutralize the UA popover box (the picker draws its own card in its
412
+ // shadow root); Floating UI drives the position in _positionEmojiPicker.
372
413
  picker.setAttribute('popover', 'auto');
373
414
  Object.assign(picker.style, {
374
415
  position: 'fixed',
@@ -377,10 +418,22 @@ export class ProseEditor extends LuxenFormAssociatedElement {
377
418
  padding: '0',
378
419
  border: '0',
379
420
  background: 'transparent',
421
+ // emoji-picker-element ships `:host { display: flex }` (author origin),
422
+ // which overrides the UA `[popover]:not(:popover-open) { display: none }`
423
+ // rule — so a *closed* popover would otherwise stay laid out and visible.
424
+ // Drive `display` from the popover state via inline style (which beats the
425
+ // shadow `:host` rule) so the picker is truly hidden when closed, on every
426
+ // dismissal path (light-dismiss, Escape, re-click, select).
427
+ display: 'none',
380
428
  });
429
+ picker.style.setProperty('--border-radius', '0.5rem');
381
430
  picker.addEventListener('toggle', (event) => {
382
- if (event.newState === 'open')
383
- void this._positionEmojiPicker();
431
+ const open = event.newState === 'open';
432
+ picker.style.display = open ? 'flex' : 'none';
433
+ if (open)
434
+ this._startEmojiAutoUpdate();
435
+ else
436
+ this._stopEmojiAutoUpdate();
384
437
  });
385
438
  this._emojiPicker = picker;
386
439
  return picker;
@@ -413,11 +466,31 @@ export class ProseEditor extends LuxenFormAssociatedElement {
413
466
  get _emojiButton() {
414
467
  return this.shadowRoot?.querySelector('[data-command="emoji"]') ?? null;
415
468
  }
469
+ /**
470
+ * Keep the picker pinned to the emoji button while it is open. Floating UI's
471
+ * `autoUpdate` re-runs `_positionEmojiPicker` on scroll (including ancestor
472
+ * scroll), resize, and layout shifts, so the picker stays anchored to the
473
+ * button — the cross-tree equivalent of CSS anchor positioning, which can't be
474
+ * used here because the button lives in this element's shadow root while the
475
+ * picker is parented to the top-layer container (a different tree scope).
476
+ */
477
+ _startEmojiAutoUpdate() {
478
+ const button = this._emojiButton;
479
+ const picker = this._emojiPicker;
480
+ if (!button || !picker)
481
+ return;
482
+ this._stopEmojiAutoUpdate();
483
+ this._emojiAutoUpdateCleanup = autoUpdate(button, picker, () => void this._positionEmojiPicker());
484
+ }
485
+ _stopEmojiAutoUpdate() {
486
+ this._emojiAutoUpdateCleanup?.();
487
+ this._emojiAutoUpdateCleanup = undefined;
488
+ }
416
489
  async _positionEmojiPicker() {
417
490
  const button = this._emojiButton;
418
491
  const picker = this._emojiPicker;
419
- // Runs from the picker's `toggle` event once it is open, so it is already
420
- // laid out in the top-layer and can be measured.
492
+ // Driven by `autoUpdate` while the picker is open, so it is already laid out
493
+ // in the top-layer and can be measured.
421
494
  if (!button || !picker || !picker.matches(':popover-open'))
422
495
  return;
423
496
  const { x, y } = await computePosition(button, picker, {
@@ -520,7 +593,7 @@ export class ProseEditor extends LuxenFormAssociatedElement {
520
593
  `;
521
594
  }
522
595
  }
523
- _ProseEditor_initialHtml_accessor_storage = new WeakMap(), _ProseEditor_initialJson_accessor_storage = new WeakMap(), _ProseEditor_editorClass_accessor_storage = new WeakMap(), _ProseEditor_toolbar_accessor_storage = new WeakMap(), _ProseEditor_toolbarPreset_accessor_storage = new WeakMap(), _ProseEditor_toolbarPlacement_accessor_storage = new WeakMap(), _ProseEditor_autofocus_accessor_storage = new WeakMap(), _ProseEditor_placeholder_accessor_storage = new WeakMap();
596
+ _ProseEditor_initialHtml_accessor_storage = new WeakMap(), _ProseEditor_initialJson_accessor_storage = new WeakMap(), _ProseEditor_editorClass_accessor_storage = new WeakMap(), _ProseEditor_toolbar_accessor_storage = new WeakMap(), _ProseEditor_toolbarPreset_accessor_storage = new WeakMap(), _ProseEditor_toolbarPlacement_accessor_storage = new WeakMap(), _ProseEditor_autofocus_accessor_storage = new WeakMap(), _ProseEditor_placeholder_accessor_storage = new WeakMap(), _ProseEditor_emojiDataSource_accessor_storage = new WeakMap();
524
597
  ProseEditor.styles = [hostStyles, styles];
525
598
  __decorate([
526
599
  property({ attribute: 'initial-html' })
@@ -551,3 +624,6 @@ __decorate([
551
624
  __decorate([
552
625
  property()
553
626
  ], ProseEditor.prototype, "placeholder", null);
627
+ __decorate([
628
+ property({ attribute: 'emoji-data-source' })
629
+ ], ProseEditor.prototype, "emojiDataSource", null);
@@ -86,7 +86,13 @@
86
86
  display: flex;
87
87
  align-items: center;
88
88
  gap: 0.375rem;
89
- overflow: hidden;
89
+ /* `clip` (not `hidden`) so the label still truncates long text to an ellipsis
90
+ while `overflow-clip-margin` lets a slotted interactive control's hover/focus
91
+ decoration (a row-action button, an `<l-dropdown>` trigger) bleed past the
92
+ row-height label box instead of being cut on its top/bottom/left edges. The
93
+ margin is kept small so the bleed stays imperceptible for text. */
94
+ overflow: clip;
95
+ overflow-clip-margin: 0.375rem;
90
96
  text-overflow: ellipsis;
91
97
  white-space: nowrap;
92
98
  }
@@ -72,6 +72,11 @@
72
72
  }
73
73
  ],
74
74
  "cssProperties": [
75
+ {
76
+ "name": "--size",
77
+ "default": "40px",
78
+ "description": "Box size (any length). Set it inline (e.g. `style=\"--size: 20px\"`) for an arbitrary size beyond the `size` token scale; the font then follows proportionally. The `size` attribute sets it to the matching token."
79
+ },
75
80
  {
76
81
  "name": "--color",
77
82
  "default": null,
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.9.2",
2
+ "version": "0.10.0",
3
3
  "elements": [
4
4
  {
5
5
  "name": "avatar",
@@ -75,6 +75,11 @@
75
75
  }
76
76
  ],
77
77
  "cssProperties": [
78
+ {
79
+ "name": "--size",
80
+ "default": "40px",
81
+ "description": "Box size (any length). Set it inline (e.g. `style=\"--size: 20px\"`) for an arbitrary size beyond the `size` token scale; the font then follows proportionally. The `size` attribute sets it to the matching token."
82
+ },
78
83
  {
79
84
  "name": "--color",
80
85
  "default": null,
@@ -2415,6 +2420,14 @@
2415
2420
  "reflects": false,
2416
2421
  "description": "Placeholder shown when the editor is empty."
2417
2422
  },
2423
+ {
2424
+ "name": "emojiDataSource",
2425
+ "attribute": "emoji-data-source",
2426
+ "type": "string",
2427
+ "default": "''",
2428
+ "reflects": false,
2429
+ "description": "URL the emoji picker fetches its data from. Point this at a locally served\n`emojibase-data` JSON to run fully offline (no CDN). Defaults to the\npicker's bundled CDN source."
2430
+ },
2418
2431
  {
2419
2432
  "name": "validationTarget",
2420
2433
  "attribute": null,