@emdash-cms/admin 0.24.1 → 0.26.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 (38) hide show
  1. package/dist/{LocaleDirectionProvider-CK7aJ2Lb.js → LocaleDirectionProvider-elsAJo9B.js} +23 -11
  2. package/dist/LocaleDirectionProvider-elsAJo9B.js.map +1 -0
  3. package/dist/{config-CPRInh7U.d.ts → config-DpQrGzsu.d.ts} +1 -1
  4. package/dist/{config-CPRInh7U.d.ts.map → config-DpQrGzsu.d.ts.map} +1 -1
  5. package/dist/index.d.ts +14 -1
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +178 -117
  8. package/dist/index.js.map +1 -1
  9. package/dist/locales/hu/messages.mjs +1 -0
  10. package/dist/locales/index.d.ts +1 -1
  11. package/dist/locales/index.js +1 -1
  12. package/dist/locales/tr/messages.mjs +1 -0
  13. package/dist/{messages-DjQX9PJY.js → messages-3sesjT8a.js} +1 -1
  14. package/dist/{messages-DjQX9PJY.js.map → messages-3sesjT8a.js.map} +1 -1
  15. package/dist/{messages-Bh2bSu3x.js → messages-BBhuGu6K.js} +1 -1
  16. package/dist/{messages-Bh2bSu3x.js.map → messages-BBhuGu6K.js.map} +1 -1
  17. package/dist/{messages-D6wHPGil.js → messages-C6_69h5z.js} +1 -1
  18. package/dist/{messages-D6wHPGil.js.map → messages-C6_69h5z.js.map} +1 -1
  19. package/dist/{messages-n6HQSjDA.js → messages-ChEF0PwF.js} +1 -1
  20. package/dist/{messages-n6HQSjDA.js.map → messages-ChEF0PwF.js.map} +1 -1
  21. package/dist/{messages-cWzWbWxk.js → messages-Cqpd51ck.js} +1 -1
  22. package/dist/{messages-cWzWbWxk.js.map → messages-Cqpd51ck.js.map} +1 -1
  23. package/dist/{messages-Crei9McK.js → messages-DBzgje1G.js} +1 -1
  24. package/dist/{messages-Crei9McK.js.map → messages-DBzgje1G.js.map} +1 -1
  25. package/dist/{messages-B43-WzBC.js → messages-DEC9v1nR.js} +1 -1
  26. package/dist/{messages-B43-WzBC.js.map → messages-DEC9v1nR.js.map} +1 -1
  27. package/dist/{messages-BTDhbVyJ.js → messages-DOJAmOSK.js} +1 -1
  28. package/dist/{messages-BTDhbVyJ.js.map → messages-DOJAmOSK.js.map} +1 -1
  29. package/dist/messages-DjTHn-jd.js +6 -0
  30. package/dist/messages-DjTHn-jd.js.map +1 -0
  31. package/dist/{messages-BdciNNnG.js → messages-KiGKwzQW.js} +1 -1
  32. package/dist/{messages-BdciNNnG.js.map → messages-KiGKwzQW.js.map} +1 -1
  33. package/dist/{messages-DlzFD1ts.js → messages-RWuknKyd.js} +1 -1
  34. package/dist/{messages-DlzFD1ts.js.map → messages-RWuknKyd.js.map} +1 -1
  35. package/dist/messages-itTyZdsM.js +6 -0
  36. package/dist/messages-itTyZdsM.js.map +1 -0
  37. package/package.json +2 -2
  38. package/dist/LocaleDirectionProvider-CK7aJ2Lb.js.map +0 -1
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { c as fetchAuthMode, d as throwResponseError, i as fetchPlugins, l as fetchManifest, n as enablePlugin, o as API_BASE, r as fetchPlugin, s as apiFetch, t as disablePlugin, u as parseApiResponse } from "./plugins-G9Z9NQ7n.js";
2
- import { a as SUPPORTED_LOCALES, i as DEFAULT_LOCALE, l as resolveLocale, n as useLocale, o as SUPPORTED_LOCALE_CODES, s as getLocaleDir, t as LocaleDirectionProvider } from "./LocaleDirectionProvider-CK7aJ2Lb.js";
2
+ import { a as SUPPORTED_LOCALES, i as DEFAULT_LOCALE, l as resolveLocale, n as useLocale, o as SUPPORTED_LOCALE_CODES, s as getLocaleDir, t as LocaleDirectionProvider } from "./LocaleDirectionProvider-elsAJo9B.js";
3
3
  import "./locales/index.js";
4
- import { Autocomplete, Badge, Button, Checkbox, ClipboardText, CommandPalette, Dialog, Input, InputArea, Label, LinkButton, LinkProvider, Loader, Popover, Select, Sidebar as KumoSidebar, Switch, Tabs, Toast, Toasty, Tooltip, buttonVariants, useSidebar } from "@cloudflare/kumo";
4
+ import { Autocomplete, Badge, Button, Checkbox, ClipboardText, Combobox, CommandPalette, Dialog, Input, InputArea, Label, LinkButton, LinkProvider, Loader, Popover, Select, Sidebar as KumoSidebar, Switch, Tabs, Toast, Toasty, Tooltip, buttonVariants, useSidebar } from "@cloudflare/kumo";
5
5
  import { i18n } from "@lingui/core";
6
6
  import { I18nProvider, Trans, useLingui } from "@lingui/react";
7
7
  import { QueryClient, QueryClientProvider, keepPreviousData, useInfiniteQuery, useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
@@ -3187,6 +3187,11 @@ function useDebouncedValue$1(value, delay) {
3187
3187
 
3188
3188
  //#endregion
3189
3189
  //#region src/lib/media-utils.ts
3190
+ /** Read a string value from an untyped `meta` bag, or undefined. */
3191
+ function metaString(meta, key) {
3192
+ const value = meta?.[key];
3193
+ return typeof value === "string" ? value : void 0;
3194
+ }
3190
3195
  function providerItemToMediaItem(providerId, item) {
3191
3196
  return {
3192
3197
  id: item.id,
@@ -3196,6 +3201,8 @@ function providerItemToMediaItem(providerId, item) {
3196
3201
  size: item.size || 0,
3197
3202
  width: item.width,
3198
3203
  height: item.height,
3204
+ blurhash: item.blurhash ?? metaString(item.meta, "blurhash"),
3205
+ dominantColor: item.dominantColor ?? metaString(item.meta, "dominantColor"),
3199
3206
  alt: item.alt,
3200
3207
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
3201
3208
  provider: providerId,
@@ -4579,6 +4586,8 @@ function ImageFieldRenderer({ id, label, description, value, onChange, required,
4579
4586
  alt: item.alt || "",
4580
4587
  width: item.width,
4581
4588
  height: item.height,
4589
+ blurhash: item.blurhash ?? metaString(item.meta, "blurhash"),
4590
+ dominantColor: item.dominantColor ?? metaString(item.meta, "dominantColor"),
4582
4591
  meta: isLocalProvider ? {
4583
4592
  ...item.meta,
4584
4593
  storageKey: item.storageKey
@@ -4990,18 +4999,26 @@ function SubFieldInput({ subField, value, onChange }) {
4990
4999
  onChange: (e) => onChange(fromDatetimeLocalInputValue(e.target.value)),
4991
5000
  required: subField.required
4992
5001
  });
4993
- case "select": return /* @__PURE__ */ jsx(Select, {
4994
- label: subField.label,
4995
- value: typeof value === "string" ? value : "",
4996
- onValueChange: (v) => onChange(v ?? ""),
4997
- items: {
4998
- "": _t3({
5002
+ case "select": {
5003
+ const options = Array.isArray(subField.options) ? subField.options : [];
5004
+ return /* @__PURE__ */ jsxs(Combobox, {
5005
+ label: subField.label,
5006
+ value: typeof value === "string" && value ? value : null,
5007
+ onValueChange: (v) => onChange(typeof v === "string" ? v : ""),
5008
+ items: options,
5009
+ required: subField.required,
5010
+ children: [/* @__PURE__ */ jsx(Combobox.TriggerInput, { placeholder: _t3({
4999
5011
  id: "O/7I0o",
5000
5012
  message: "Select..."
5001
- }),
5002
- ...Object.fromEntries((subField.options ?? []).map((opt) => [opt, opt]))
5003
- }
5004
- });
5013
+ }) }), /* @__PURE__ */ jsxs(Combobox.Content, { children: [/* @__PURE__ */ jsx(Combobox.Empty, { children: _t3({
5014
+ id: "Ev2r9A",
5015
+ message: "No results"
5016
+ }) }), /* @__PURE__ */ jsx(Combobox.List, { children: (opt) => /* @__PURE__ */ jsx(Combobox.Item, {
5017
+ value: opt,
5018
+ children: opt
5019
+ }, opt) })] })]
5020
+ });
5021
+ }
5005
5022
  case "image": return /* @__PURE__ */ jsx(ImageFieldRenderer, {
5006
5023
  label: subField.label,
5007
5024
  value: value != null && typeof value === "object" ? value : typeof value === "string" && value ? value : void 0,
@@ -5105,6 +5122,8 @@ function ImageDetailPanel({ attributes, onUpdate, onReplace, onDelete, onClose,
5105
5122
  mediaId: item.id,
5106
5123
  width: item.width,
5107
5124
  height: item.height,
5125
+ blurhash: item.blurhash,
5126
+ dominantColor: item.dominantColor,
5108
5127
  caption: void 0,
5109
5128
  title: void 0
5110
5129
  });
@@ -6391,6 +6410,16 @@ function languageLabelDescriptor(value) {
6391
6410
  * so that less common languages can still be used. Free-form input is
6392
6411
  * sanitized to a single safe CSS class token via `normalizeLanguage` so the
6393
6412
  * frontend's `language-{id}` class stays well-formed.
6413
+ *
6414
+ * The popover content is rendered through Kumo's `Popover`, which portals it
6415
+ * out of the editor's contentEditable DOM. That portal is load-bearing, not
6416
+ * cosmetic: a code block is a non-atom ProseMirror node with live editable
6417
+ * content, so if the picker's text input lived inside the node view, typing
6418
+ * would move the DOM selection into it. ProseMirror reads that selection,
6419
+ * dispatches a selection-correcting transaction, and the resulting node-view
6420
+ * redraw recreates this React component mid-edit, tearing the picker down --
6421
+ * the "language picker loses focus and closes when you type" bug (issue
6422
+ * #1200). Keeping the input outside the editor DOM avoids it entirely.
6394
6423
  */
6395
6424
  function CodeBlockNodeView({ node, updateAttributes, selected }) {
6396
6425
  const { _: _t } = useLingui();
@@ -6412,7 +6441,6 @@ function CodeBlockNodeView({ node, updateAttributes, selected }) {
6412
6441
  return lang.aliases?.some((alias) => alias.toLowerCase().includes(searchText)) ?? false;
6413
6442
  }, [findLanguageByDisplayLabel, _t]);
6414
6443
  const [draft, setDraft] = React$1.useState(() => labelText(storedLanguage));
6415
- const popoverRef = React$1.useRef(null);
6416
6444
  React$1.useEffect(() => {
6417
6445
  if (!isEditing) setDraft(labelText(storedLanguage));
6418
6446
  }, [
@@ -6441,20 +6469,8 @@ function CodeBlockNodeView({ node, updateAttributes, selected }) {
6441
6469
  if (e.key === "Enter") {
6442
6470
  e.preventDefault();
6443
6471
  commit();
6444
- } else if (e.key === "Escape") {
6445
- e.preventDefault();
6446
- closePicker();
6447
6472
  }
6448
6473
  };
6449
- React$1.useEffect(() => {
6450
- if (!isEditing) return void 0;
6451
- const onMouseDown = (event) => {
6452
- const target = event.target instanceof Node ? event.target : null;
6453
- if (popoverRef.current && target && !popoverRef.current.contains(target)) closePicker();
6454
- };
6455
- document.addEventListener("mousedown", onMouseDown);
6456
- return () => document.removeEventListener("mousedown", onMouseDown);
6457
- }, [isEditing, closePicker]);
6458
6474
  const label = labelText(storedLanguage);
6459
6475
  const chipPersistent = isEditing || Boolean(storedLanguage) || selected;
6460
6476
  return /* @__PURE__ */ jsxs(NodeViewWrapper, {
@@ -6466,89 +6482,95 @@ function CodeBlockNodeView({ node, updateAttributes, selected }) {
6466
6482
  }), /* @__PURE__ */ jsx("div", {
6467
6483
  className: "absolute end-2 top-2 select-none",
6468
6484
  contentEditable: false,
6469
- children: isEditing ? /* @__PURE__ */ jsxs("div", {
6470
- ref: popoverRef,
6471
- className: "flex items-center gap-1 rounded-md border bg-kumo-overlay p-1 shadow-lg",
6472
- onKeyDown: handleKeyDown,
6473
- children: [
6474
- /* @__PURE__ */ jsxs(Autocomplete, {
6475
- items: languageItems,
6476
- value: draft,
6477
- onValueChange: (next) => setDraft(next),
6478
- filter: filterLanguages,
6479
- children: [/* @__PURE__ */ jsx(Autocomplete.InputGroup, {
6480
- size: "sm",
6481
- placeholder: _t({
6482
- id: "vXIe7J",
6483
- message: "Language"
6484
- })
6485
- }), /* @__PURE__ */ jsxs(Autocomplete.Content, {
6486
- sideOffset: 4,
6487
- children: [/* @__PURE__ */ jsx(Autocomplete.List, { children: (item) => /* @__PURE__ */ jsx(Autocomplete.Item, {
6488
- value: item,
6489
- children: item
6490
- }, item) }), /* @__PURE__ */ jsx(Autocomplete.Empty, { children: _t({
6491
- id: "4TVVsS",
6492
- message: "No matches"
6493
- }) })]
6494
- })]
6485
+ children: /* @__PURE__ */ jsxs(Popover, {
6486
+ open: isEditing,
6487
+ onOpenChange: (open) => open ? openPicker() : closePicker(),
6488
+ children: [/* @__PURE__ */ jsx(Popover.Trigger, { render: /* @__PURE__ */ jsx("button", {
6489
+ type: "button",
6490
+ tabIndex: chipPersistent ? 0 : -1,
6491
+ onMouseDown: (e) => e.preventDefault(),
6492
+ className: "rounded-md border bg-kumo-overlay/90 px-2 py-1 text-xs text-kumo-subtle opacity-0 transition-opacity hover:text-kumo-strong focus:opacity-100 focus:outline-none focus:ring-2 focus:ring-kumo-brand group-hover:opacity-100 data-[persistent=true]:opacity-100",
6493
+ "data-persistent": chipPersistent ? "true" : "false",
6494
+ title: _t({
6495
+ id: "9cbOH7",
6496
+ message: "Set language"
6495
6497
  }),
6496
- /* @__PURE__ */ jsx(Button, {
6497
- type: "button",
6498
- variant: "ghost",
6499
- shape: "square",
6500
- className: "h-7 w-7",
6501
- onMouseDown: (e) => e.preventDefault(),
6502
- onClick: () => commit(),
6503
- title: _t({
6504
- id: "Ek42QX",
6505
- message: "Apply language"
6506
- }),
6507
- "aria-label": _t({
6508
- id: "Ek42QX",
6509
- message: "Apply language"
6510
- }),
6511
- children: /* @__PURE__ */ jsx(Check, { className: "h-4 w-4" })
6498
+ "aria-label": _t({
6499
+ id: "q/A3pF",
6500
+ message: "Set language (current: {label})",
6501
+ values: { label }
6512
6502
  }),
6513
- /* @__PURE__ */ jsx(Button, {
6514
- type: "button",
6515
- variant: "ghost",
6516
- shape: "square",
6517
- className: "h-7 w-7",
6518
- onMouseDown: (e) => e.preventDefault(),
6519
- onClick: closePicker,
6520
- title: _t({
6521
- id: "dEgA5A",
6522
- message: "Cancel"
6523
- }),
6524
- "aria-label": _t({
6525
- id: "dEgA5A",
6526
- message: "Cancel"
6527
- }),
6528
- children: /* @__PURE__ */ jsx(X, { className: "h-4 w-4" })
6503
+ "aria-hidden": chipPersistent ? void 0 : true,
6504
+ children: storedLanguage ? label : _t({
6505
+ id: "9cbOH7",
6506
+ message: "Set language"
6507
+ })
6508
+ }) }), /* @__PURE__ */ jsx(Popover.Content, {
6509
+ side: "bottom",
6510
+ className: "w-auto p-1",
6511
+ children: /* @__PURE__ */ jsxs("div", {
6512
+ className: "flex items-center gap-1",
6513
+ onKeyDown: handleKeyDown,
6514
+ children: [
6515
+ /* @__PURE__ */ jsxs(Autocomplete, {
6516
+ items: languageItems,
6517
+ value: draft,
6518
+ onValueChange: (next) => setDraft(next),
6519
+ filter: filterLanguages,
6520
+ children: [/* @__PURE__ */ jsx(Autocomplete.InputGroup, {
6521
+ size: "sm",
6522
+ placeholder: _t({
6523
+ id: "vXIe7J",
6524
+ message: "Language"
6525
+ })
6526
+ }), /* @__PURE__ */ jsxs(Autocomplete.Content, {
6527
+ sideOffset: 4,
6528
+ children: [/* @__PURE__ */ jsx(Autocomplete.List, { children: (item) => /* @__PURE__ */ jsx(Autocomplete.Item, {
6529
+ value: item,
6530
+ children: item
6531
+ }, item) }), /* @__PURE__ */ jsx(Autocomplete.Empty, { children: _t({
6532
+ id: "4TVVsS",
6533
+ message: "No matches"
6534
+ }) })]
6535
+ })]
6536
+ }),
6537
+ /* @__PURE__ */ jsx(Button, {
6538
+ type: "button",
6539
+ variant: "ghost",
6540
+ shape: "square",
6541
+ className: "h-7 w-7",
6542
+ onMouseDown: (e) => e.preventDefault(),
6543
+ onClick: () => commit(),
6544
+ title: _t({
6545
+ id: "Ek42QX",
6546
+ message: "Apply language"
6547
+ }),
6548
+ "aria-label": _t({
6549
+ id: "Ek42QX",
6550
+ message: "Apply language"
6551
+ }),
6552
+ children: /* @__PURE__ */ jsx(Check, { className: "h-4 w-4" })
6553
+ }),
6554
+ /* @__PURE__ */ jsx(Button, {
6555
+ type: "button",
6556
+ variant: "ghost",
6557
+ shape: "square",
6558
+ className: "h-7 w-7",
6559
+ onMouseDown: (e) => e.preventDefault(),
6560
+ onClick: closePicker,
6561
+ title: _t({
6562
+ id: "dEgA5A",
6563
+ message: "Cancel"
6564
+ }),
6565
+ "aria-label": _t({
6566
+ id: "dEgA5A",
6567
+ message: "Cancel"
6568
+ }),
6569
+ children: /* @__PURE__ */ jsx(X, { className: "h-4 w-4" })
6570
+ })
6571
+ ]
6529
6572
  })
6530
- ]
6531
- }) : /* @__PURE__ */ jsx("button", {
6532
- type: "button",
6533
- tabIndex: chipPersistent ? 0 : -1,
6534
- onMouseDown: (e) => e.preventDefault(),
6535
- onClick: openPicker,
6536
- className: "rounded-md border bg-kumo-overlay/90 px-2 py-1 text-xs text-kumo-subtle opacity-0 transition-opacity hover:text-kumo-strong focus:opacity-100 focus:outline-none focus:ring-2 focus:ring-kumo-brand group-hover:opacity-100 data-[persistent=true]:opacity-100",
6537
- "data-persistent": chipPersistent ? "true" : "false",
6538
- title: _t({
6539
- id: "9cbOH7",
6540
- message: "Set language"
6541
- }),
6542
- "aria-label": _t({
6543
- id: "q/A3pF",
6544
- message: "Set language (current: {label})",
6545
- values: { label }
6546
- }),
6547
- "aria-hidden": chipPersistent ? void 0 : true,
6548
- children: storedLanguage ? label : _t({
6549
- id: "9cbOH7",
6550
- message: "Set language"
6551
- })
6573
+ })]
6552
6574
  })
6553
6575
  })]
6554
6576
  });
@@ -7084,6 +7106,8 @@ function ImageNodeView({ node, updateAttributes, selected, deleteNode, editor })
7084
7106
  mediaId: node.attrs.mediaId,
7085
7107
  width: node.attrs.width,
7086
7108
  height: node.attrs.height,
7109
+ blurhash: node.attrs.blurhash,
7110
+ dominantColor: node.attrs.dominantColor,
7087
7111
  displayWidth: node.attrs.displayWidth,
7088
7112
  displayHeight: node.attrs.displayHeight,
7089
7113
  alignment: node.attrs.alignment
@@ -7309,6 +7333,8 @@ const ImageExtension = Node$2.create({
7309
7333
  provider: { default: null },
7310
7334
  width: { default: null },
7311
7335
  height: { default: null },
7336
+ blurhash: { default: null },
7337
+ dominantColor: { default: null },
7312
7338
  displayWidth: { default: null },
7313
7339
  displayHeight: { default: null },
7314
7340
  alignment: { default: null }
@@ -8028,6 +8054,8 @@ function convertPMNode(node) {
8028
8054
  case "image": {
8029
8055
  const attrs = node.attrs ?? {};
8030
8056
  const provider = attrStr(attrs.provider);
8057
+ const blurhash = attrStr(attrs.blurhash);
8058
+ const dominantColor = attrStr(attrs.dominantColor);
8031
8059
  return {
8032
8060
  _type: "image",
8033
8061
  _key: generateKey(),
@@ -8040,6 +8068,8 @@ function convertPMNode(node) {
8040
8068
  caption: attrStr(attrs.caption) ?? attrStr(attrs.title),
8041
8069
  width: attrNum(attrs.width),
8042
8070
  height: attrNum(attrs.height),
8071
+ ...blurhash ? { blurhash } : {},
8072
+ ...dominantColor ? { dominantColor } : {},
8043
8073
  displayWidth: attrNum(attrs.displayWidth),
8044
8074
  displayHeight: attrNum(attrs.displayHeight),
8045
8075
  alignment: attrStr(attrs.alignment)
@@ -8262,6 +8292,9 @@ function convertPTBlock(block) {
8262
8292
  case "image": {
8263
8293
  if (!isImageBlock(block)) return null;
8264
8294
  const imageBlock = block;
8295
+ const meta = imageBlock.asset.meta;
8296
+ const blurhash = typeof imageBlock.blurhash === "string" ? imageBlock.blurhash : typeof meta?.blurhash === "string" ? meta.blurhash : null;
8297
+ const dominantColor = typeof imageBlock.dominantColor === "string" ? imageBlock.dominantColor : typeof meta?.dominantColor === "string" ? meta.dominantColor : null;
8265
8298
  return {
8266
8299
  type: "image",
8267
8300
  attrs: {
@@ -8272,6 +8305,8 @@ function convertPTBlock(block) {
8272
8305
  mediaId: imageBlock.asset._ref,
8273
8306
  width: imageBlock.width,
8274
8307
  height: imageBlock.height,
8308
+ blurhash,
8309
+ dominantColor,
8275
8310
  displayWidth: imageBlock.displayWidth,
8276
8311
  displayHeight: imageBlock.displayHeight,
8277
8312
  alignment: imageBlock.alignment
@@ -9297,28 +9332,49 @@ function DynamicSelect({ field, pluginId, value, onChange }) {
9297
9332
  }
9298
9333
  })] });
9299
9334
  }
9335
+ const WORDS_PER_MINUTE = 200;
9336
+ const CJK_CHARACTERS_PER_MINUTE = 500;
9337
+ const WHITESPACE_REGEX$1 = /\s+/;
9338
+ const CJK_CHARACTER_REGEX = /\p{Script=Han}|\p{Script=Hangul}|\p{Script=Hiragana}|\p{Script=Katakana}/gu;
9339
+ function countCjkCharacters(text) {
9340
+ return text.match(CJK_CHARACTER_REGEX)?.length ?? 0;
9341
+ }
9342
+ function countNonCjkWords(text) {
9343
+ return text.replace(CJK_CHARACTER_REGEX, " ").split(WHITESPACE_REGEX$1).filter(Boolean).length;
9344
+ }
9345
+ /**
9346
+ * Word count for the editor footer. CJK characters are counted individually
9347
+ * because they are not delimited by spaces; other scripts are counted by word.
9348
+ * Used as the `wordCounter` for the CharacterCount extension, whose default
9349
+ * (`text.split(' ')`) reports a spaceless CJK paragraph as a single word.
9350
+ */
9351
+ function countWords(text) {
9352
+ return countNonCjkWords(text) + countCjkCharacters(text);
9353
+ }
9300
9354
  /**
9301
- * Calculate reading time in minutes based on word count
9302
- * Uses a standard reading speed of 200 words per minute
9355
+ * Calculate reading time in minutes for the given text. Word-based scripts are
9356
+ * read at WORDS_PER_MINUTE and CJK characters at CJK_CHARACTERS_PER_MINUTE.
9357
+ * Returns 0 for an empty document.
9303
9358
  */
9304
- function calculateReadingTime(words) {
9305
- return Math.ceil(words / 200);
9359
+ function calculateReadingTime(text) {
9360
+ return Math.ceil(countNonCjkWords(text) / WORDS_PER_MINUTE + countCjkCharacters(text) / CJK_CHARACTERS_PER_MINUTE);
9306
9361
  }
9307
9362
  /**
9308
9363
  * Editor footer showing writing metrics (word count, character count, reading time)
9309
9364
  */
9310
9365
  function EditorFooter({ editor }) {
9311
- const { words, characters } = useEditorState({
9366
+ const { words, characters, text } = useEditorState({
9312
9367
  editor,
9313
9368
  selector: (ctx) => {
9314
9369
  const storage = ctx.editor.storage.characterCount;
9315
9370
  return {
9316
9371
  words: storage.words(),
9317
- characters: storage.characters()
9372
+ characters: storage.characters(),
9373
+ text: ctx.editor.getText()
9318
9374
  };
9319
9375
  }
9320
9376
  });
9321
- const readingTime = calculateReadingTime(words);
9377
+ const readingTime = calculateReadingTime(text);
9322
9378
  return /* @__PURE__ */ jsxs("div", {
9323
9379
  className: "border-t px-4 py-2 flex items-center gap-4 text-xs text-kumo-subtle",
9324
9380
  children: [
@@ -9501,7 +9557,7 @@ function PortableTextEditor({ value, onChange, placeholder = "Start writing...",
9501
9557
  onStateChange: setSlashMenuState,
9502
9558
  getState: () => slashMenuStateRef.current
9503
9559
  }),
9504
- CharacterCount,
9560
+ CharacterCount.configure({ wordCounter: countWords }),
9505
9561
  Focus.configure({
9506
9562
  className: "has-focus",
9507
9563
  mode: "all"
@@ -9575,7 +9631,9 @@ function PortableTextEditor({ value, onChange, placeholder = "Start writing...",
9575
9631
  mediaId: item.id,
9576
9632
  provider: item.provider || "local",
9577
9633
  width: item.width,
9578
- height: item.height
9634
+ height: item.height,
9635
+ blurhash: item.blurhash,
9636
+ dominantColor: item.dominantColor
9579
9637
  }).run();
9580
9638
  setMediaPickerOpen(false);
9581
9639
  }, [editor]);
@@ -10009,7 +10067,9 @@ function EditorToolbar({ editor, focusMode, onFocusModeChange }) {
10009
10067
  alt: item.alt || item.filename,
10010
10068
  mediaId: item.id,
10011
10069
  width: item.width,
10012
- height: item.height
10070
+ height: item.height,
10071
+ blurhash: item.blurhash,
10072
+ dominantColor: item.dominantColor
10013
10073
  }).run();
10014
10074
  }, [editor]);
10015
10075
  const handleKeyDown = React$1.useCallback((e) => {
@@ -30744,7 +30804,7 @@ function Header() {
30744
30804
  /* @__PURE__ */ jsx("hr", { className: "my-1" }),
30745
30805
  /* @__PURE__ */ jsxs("button", {
30746
30806
  onClick: handleLogout,
30747
- className: "flex items-center gap-2 rounded-md px-3 py-2 text-sm text-kumo-danger hover:bg-kumo-danger/10 w-full text-start",
30807
+ className: "flex items-center gap-2 rounded-md px-3 py-2 text-sm text-kumo-danger hover:bg-kumo-danger/10 w-full text-start cursor-pointer",
30748
30808
  children: [/* @__PURE__ */ jsx(SignOut, { className: "h-4 w-4" }), _t({
30749
30809
  id: "FgAxTj",
30750
30810
  message: "Log out"
@@ -39546,6 +39606,7 @@ function ContentListPage() {
39546
39606
  return data?.pages.flatMap((page) => page.items) || [];
39547
39607
  }, [data]);
39548
39608
  const total = data?.pages[0]?.total ?? items.length;
39609
+ const handleLoadMore = React$1.useCallback(() => void fetchNextPage(), [fetchNextPage]);
39549
39610
  if (!manifest) return /* @__PURE__ */ jsx(LoadingScreen, {});
39550
39611
  const collectionConfig = manifest.collections[collection];
39551
39612
  if (!collectionConfig) return /* @__PURE__ */ jsx(NotFoundPage, { message: `Collection "${collection}" not found` });
@@ -39565,7 +39626,7 @@ function ContentListPage() {
39565
39626
  isLoading: isLoading || isFetchingNextPage,
39566
39627
  isTrashedLoading,
39567
39628
  hasMore: !!hasNextPage,
39568
- onLoadMore: React$1.useCallback(() => void fetchNextPage(), [fetchNextPage]),
39629
+ onLoadMore: handleLoadMore,
39569
39630
  trashedCount: trashedData?.items?.length || 0,
39570
39631
  onDelete: (id) => deleteMutation.mutate(id),
39571
39632
  onRestore: (id) => restoreMutation.mutate(id),