@homecode/ui 5.20.20 → 5.20.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/esm/index.js CHANGED
@@ -24,12 +24,13 @@ export { Flex } from './src/components/Flex/Flex.js';
24
24
  export { FoldedLine } from './src/components/FoldedLine/FoldedLine.js';
25
25
  export { FoldedLineAction, FoldedLineActionsProvider, useFoldedLineActionsRegister } from './src/components/FoldedLine/FoldedLineActionsContext.js';
26
26
  export { Form } from './src/components/Form/Form.js';
27
- export { FormattedText } from './src/components/FormattedText/FormattedText.js';
27
+ export { FormattedText, findJsonEndExclusive } from './src/components/FormattedText/FormattedText.js';
28
28
  export { Gap } from './src/components/Gap/Gap.js';
29
29
  export { Gallery, normalizeGalleryItem } from './src/components/Gallery/Gallery.js';
30
30
  export { GridLayout } from './src/components/GridLayout/GridLayout.js';
31
31
  export { Heading } from './src/components/Heading/Heading.js';
32
32
  export { Icon } from './src/components/Icon/Icon.js';
33
+ export { Img } from './src/components/Img/Img.js';
33
34
  export { Input } from './src/components/Input/Input.js';
34
35
  export { InputFile } from './src/components/InputFile/InputFile.js';
35
36
  export { Label } from './src/components/Label/Label.js';
@@ -64,6 +64,7 @@ import '../VideoPlayer/VideoPlayer.styl.js';
64
64
  import '../Gallery/Gallery.styl.js';
65
65
  import '../GridLayout/GridLayout.styl.js';
66
66
  import '../Heading/Heading.js';
67
+ import '../Img/Img.styl.js';
67
68
  import '../InputFile/InputFile.styl.js';
68
69
  import '../InputFile/Item/Item.styl.js';
69
70
  import '@tiptap/core';
@@ -108,7 +108,24 @@ function applyInjectors(text, injectors) {
108
108
  });
109
109
  return result;
110
110
  }
111
- function FormattedText({ text, className, onButtonClick, }) {
111
+ function FormattedText({ text, className, onButtonClick, quotes, injectors, breaks = true, }) {
112
+ // Without `<br>`s the text keeps its own newlines (pre-wrap), so a block, such
113
+ // as a table, swallows the blank lines after it instead of showing them.
114
+ const blankLinesAfter = (content, from) => breaks ? 0 : content.slice(from).match(/^\s*/)[0].length;
115
+ /** A run of `> ` lines, grayed with a bar down its left side. */
116
+ const injectQuote = (content) => {
117
+ const matches = content.match(/(?:^|\n)(>[^\n]*(?:\n>[^\n]*)*)(?:\n|$)/);
118
+ if (!matches)
119
+ return null;
120
+ const lines = matches[1]
121
+ .split('\n')
122
+ .map(line => line.replace(/^>[ \t]?/, ''));
123
+ return {
124
+ elem: (jsx("div", { className: S.quote, "data-quote": true, children: lines.join('\n') })),
125
+ index: matches.index,
126
+ length: matches[0].length,
127
+ };
128
+ };
112
129
  const injectLines = (content) => {
113
130
  const matches = content.match(/(^|\n)---[ \t]*(?:\n|$)/);
114
131
  if (!matches)
@@ -266,6 +283,7 @@ function FormattedText({ text, className, onButtonClick, }) {
266
283
  const headingTags = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'];
267
284
  const processCellContent = (value, depth = 0) => applyInjectors(value, [
268
285
  injectCodeBlock,
286
+ ...(injectors ?? []),
269
287
  injectPrettyJson,
270
288
  ...(depth === 0
271
289
  ? [
@@ -291,7 +309,10 @@ function FormattedText({ text, className, onButtonClick, }) {
291
309
  injectButtons,
292
310
  ]);
293
311
  const injectHeading = (content) => {
294
- const matches = content.match(/(^|\n)(#{1,6})\s+([^\n]+)/);
312
+ // Without `<br>`s the text keeps its own newlines (pre-wrap), so a heading,
313
+ // which is a block, swallows the blank lines around it instead of leaving
314
+ // an extra empty line.
315
+ const matches = content.match(breaks ? /(^|\n)(#{1,6})\s+([^\n]+)/ : /(^|\n+)(#{1,6})[ \t]+([^\n]+)\n*/);
295
316
  if (!matches)
296
317
  return null;
297
318
  const Tag = headingTags[Math.min(matches[2].length, 6) - 1];
@@ -350,13 +371,18 @@ function FormattedText({ text, className, onButtonClick, }) {
350
371
  return {
351
372
  elem: (jsx(Table, { size: "s", variant: "plain", columns: columns, data: tableData, className: S.table })),
352
373
  index: matches.index + (matches[1]?.length || 0),
353
- length: matches[0].length - (matches[1]?.length || 0),
374
+ length: matches[0].length -
375
+ (matches[1]?.length || 0) +
376
+ blankLinesAfter(content, matches.index + matches[0].length),
354
377
  };
355
378
  };
356
379
  return (jsx("div", { className: className, children: applyInjectors(text, [
357
380
  injectTables,
358
381
  injectCodeBlock,
382
+ ...(injectors ?? []),
359
383
  injectPrettyJson,
384
+ // After code, so a `> ` line inside a code block stays code.
385
+ ...(quotes ? [injectQuote] : []),
360
386
  injectHeading,
361
387
  injectLines,
362
388
  injectLinks,
@@ -365,8 +391,8 @@ function FormattedText({ text, className, onButtonClick, }) {
365
391
  injectBullet,
366
392
  injectNumbered,
367
393
  injectButtons,
368
- injectBreaks,
394
+ ...(breaks ? [injectBreaks] : []),
369
395
  ]) }));
370
396
  }
371
397
 
372
- export { FormattedText, hrefForFormattedLink };
398
+ export { FormattedText, findJsonEndExclusive, hrefForFormattedLink };
@@ -1,5 +1,5 @@
1
- var css_248z = ".FormattedText_bullet__vbqlq{display:inline-block;margin-bottom:2px;margin-left:var(--p-2);margin-right:2px}.FormattedText_bullet__vbqlq:before{background-color:var(--accent-color);border-radius:50%;content:\"\";display:block;height:6px;margin:0 4px;width:6px}.FormattedText_numberedIndex__rZ6Kt{font-feature-settings:\"tnum\";color:var(--accent-color);display:inline-block;font-variant-numeric:tabular-nums;margin-left:var(--p-2);margin-right:6px;min-width:1.25em}.FormattedText_line__c2bmE{background-color:var(--accent-color-alpha-200);border-radius:2px;height:2px;margin:var(--p-2) 0;width:100%}.FormattedText_codeBlock__Xrbvn,.FormattedText_codeInline__jCbNL{background-color:var(--decent-color-alpha-400);border-radius:var(--p-2);box-shadow:0 1px 1px var(--accent-color-alpha-50),inset 0 1px 1px var(--decent-color-alpha-500);font-family:monospace;-webkit-user-select:text;-moz-user-select:text;user-select:text}.FormattedText_codeInline__jCbNL{border-radius:3px;display:inline;font-size:90%;padding:0 4px}.FormattedText_codeBlock__Xrbvn,.FormattedText_jsonPre__qOGmo{display:block;font-size:90%;margin:var(--p-2) 0;overflow-x:auto;padding:var(--p-2);white-space:pre-wrap}.FormattedText_jsonPre__qOGmo{background-color:var(--decent-color-alpha-300);border-radius:var(--p-2);box-shadow:0 1px 1px var(--accent-color-alpha-50),inset 0 1px 1px var(--accent-color-alpha-50);font-family:monospace;max-width:100%;-webkit-user-select:text;-moz-user-select:text;user-select:text}.FormattedText_table__ipMMS{margin-bottom:10px}";
2
- var S = {"bullet":"FormattedText_bullet__vbqlq","numberedIndex":"FormattedText_numberedIndex__rZ6Kt","line":"FormattedText_line__c2bmE","codeInline":"FormattedText_codeInline__jCbNL","codeBlock":"FormattedText_codeBlock__Xrbvn","jsonPre":"FormattedText_jsonPre__qOGmo","table":"FormattedText_table__ipMMS"};
1
+ var css_248z = ".FormattedText_bullet__vbqlq{display:inline-block;margin-bottom:2px;margin-left:var(--p-2);margin-right:2px}.FormattedText_bullet__vbqlq:before{background-color:var(--accent-color);border-radius:50%;content:\"\";display:block;height:6px;margin:0 4px;width:6px}.FormattedText_numberedIndex__rZ6Kt{font-feature-settings:\"tnum\";color:var(--accent-color);display:inline-block;font-variant-numeric:tabular-nums;margin-left:var(--p-2);margin-right:6px;min-width:1.25em}.FormattedText_quote__BBm-M{color:var(--accent-color-alpha-600);font-size:85%;margin:0 0 var(--p-1);padding:2px 0 2px calc(4px + var(--p-2));position:relative;white-space:pre-wrap}.FormattedText_quote__BBm-M:before{background-color:var(--accent-color);border-radius:2px;bottom:2px;content:\"\";left:2px;opacity:.3;position:absolute;top:2px;width:4px}.FormattedText_line__c2bmE{background-color:var(--accent-color-alpha-200);border-radius:2px;height:2px;margin:var(--p-2) 0;width:100%}.FormattedText_codeBlock__Xrbvn,.FormattedText_codeInline__jCbNL{background-color:var(--decent-color-alpha-400);border-radius:var(--p-2);box-shadow:0 1px 1px var(--accent-color-alpha-50),inset 0 1px 1px var(--decent-color-alpha-500);font-family:monospace;-webkit-user-select:text;-moz-user-select:text;user-select:text}.FormattedText_codeInline__jCbNL{border-radius:3px;display:inline;font-size:90%;padding:0 4px}.FormattedText_codeBlock__Xrbvn,.FormattedText_jsonPre__qOGmo{display:block;font-size:90%;margin:var(--p-2) 0;overflow-x:auto;padding:var(--p-2);white-space:pre-wrap}.FormattedText_jsonPre__qOGmo{background-color:var(--decent-color-alpha-300);border-radius:var(--p-2);box-shadow:0 1px 1px var(--accent-color-alpha-50),inset 0 1px 1px var(--accent-color-alpha-50);font-family:monospace;max-width:100%;-webkit-user-select:text;-moz-user-select:text;user-select:text}.FormattedText_table__ipMMS{margin-bottom:10px}";
2
+ var S = {"bullet":"FormattedText_bullet__vbqlq","numberedIndex":"FormattedText_numberedIndex__rZ6Kt","quote":"FormattedText_quote__BBm-M","line":"FormattedText_line__c2bmE","codeInline":"FormattedText_codeInline__jCbNL","codeBlock":"FormattedText_codeBlock__Xrbvn","jsonPre":"FormattedText_jsonPre__qOGmo","table":"FormattedText_table__ipMMS"};
3
3
  if (typeof document !== 'undefined') {
4
4
  var __uilibHead = document.head || document.getElementsByTagName('head')[0];
5
5
  if (__uilibHead) {
@@ -0,0 +1,26 @@
1
+ import { jsx, Fragment } from 'react/jsx-runtime';
2
+ import cn from 'classnames';
3
+ import { useState } from 'react';
4
+ import { Icon } from '../Icon/Icon.js';
5
+ import S from './Img.styl.js';
6
+
7
+ /**
8
+ * `<img>` that never shows the browser's broken-image glyph or its alt text:
9
+ * when loading fails it renders `fallback` (a broken-image icon by default).
10
+ */
11
+ function Img({ src, alt = '', fallback, fallbackClassName, className, onError, ...rest }) {
12
+ const [failedSrc, setFailedSrc] = useState(null);
13
+ // Keyed by src, so a new src gets a fresh attempt.
14
+ const isFailed = !src || failedSrc === src;
15
+ if (isFailed) {
16
+ if (fallback !== undefined)
17
+ return jsx(Fragment, { children: fallback });
18
+ return (jsx("span", { className: cn(S.fallback, className, fallbackClassName), role: "img", "aria-label": alt || undefined, children: jsx(Icon, { type: "brokenImage", className: S.fallbackIcon }) }));
19
+ }
20
+ return (jsx("img", { ...rest, src: src, alt: alt, className: className, onError: event => {
21
+ setFailedSrc(src);
22
+ onError?.(event);
23
+ } }));
24
+ }
25
+
26
+ export { Img };
@@ -0,0 +1,20 @@
1
+ var css_248z = ".Img_fallback__crzoQ{align-items:center;color:currentColor;display:inline-flex;justify-content:center}.Img_fallbackIcon__oyxkV{height:50%;max-height:48px;max-width:48px;opacity:.4;width:50%}";
2
+ var S = {"fallback":"Img_fallback__crzoQ","fallbackIcon":"Img_fallbackIcon__oyxkV"};
3
+ if (typeof document !== 'undefined') {
4
+ var __uilibHead = document.head || document.getElementsByTagName('head')[0];
5
+ if (__uilibHead) {
6
+ var __uilibStyle = document.createElement('style');
7
+ __uilibStyle.setAttribute('data-uilib-css', '');
8
+ __uilibStyle.appendChild(document.createTextNode(css_248z));
9
+ var __uilibLink = __uilibHead.querySelector('link[rel="stylesheet"]');
10
+ var __uilibInline = __uilibHead.querySelector('style:not([data-uilib-css])');
11
+ var __uilibAnchor = __uilibLink || __uilibInline;
12
+ if (__uilibLink && __uilibInline && (__uilibLink.compareDocumentPosition(__uilibInline) & 4) === 0) {
13
+ __uilibAnchor = __uilibInline;
14
+ }
15
+ if (__uilibAnchor) __uilibHead.insertBefore(__uilibStyle, __uilibAnchor);
16
+ else __uilibHead.appendChild(__uilibStyle);
17
+ }
18
+ }
19
+
20
+ export { S as default };
@@ -66,6 +66,7 @@ import '../VideoPlayer/VideoPlayer.styl.js';
66
66
  import '../Gallery/Gallery.styl.js';
67
67
  import '../GridLayout/GridLayout.styl.js';
68
68
  import '../Heading/Heading.js';
69
+ import '../Img/Img.styl.js';
69
70
  import '../LightBox/LightBox.styl.js';
70
71
  import '../NestedMenu/NestedMenu.js';
71
72
  import '../Notifications/store.js';
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Placeholder text for the composer's empty paragraphs. Only a wholly empty
3
+ * composer shows it: a quote or list with an empty line after it already is
4
+ * content, so it keeps no hint.
5
+ */
6
+ function promptComposerPlaceholder(text) {
7
+ return ({ editor }) => (editor.isEmpty ? text : '');
8
+ }
9
+
10
+ export { promptComposerPlaceholder };
@@ -3,6 +3,7 @@ import { Placeholder } from '@tiptap/extensions';
3
3
  import { useEditor } from '@tiptap/react';
4
4
  import StarterKit from '@tiptap/starter-kit';
5
5
  import S from './PromptComposer.styl.js';
6
+ import { promptComposerPlaceholder } from './promptComposerPlaceholder.js';
6
7
  import { createPromptBlocksExtension, isInPromptBlock } from '../../tiptap/prompt-blocks/createPromptBlocksExtension.js';
7
8
  import { promptBlocksDoc } from '../../tiptap/prompt-blocks/promptBlocksText.js';
8
9
  import { createLinkHighlightExtension } from '../../tiptap/link-highlight/createLinkHighlightExtension.js';
@@ -110,7 +111,7 @@ function usePromptComposerEditor({ disabled, placeholder, slashCommandItems, sla
110
111
  }),
111
112
  NonInclusiveLink,
112
113
  Placeholder.configure({
113
- placeholder: placeholderText,
114
+ placeholder: promptComposerPlaceholder(placeholderText),
114
115
  showOnlyWhenEditable: true,
115
116
  emptyNodeClass: S.promptComposerEmptyNode,
116
117
  }),
@@ -1,4 +1,5 @@
1
1
  import React from 'react';
2
2
  import type { FormattedTextProps } from './FormattedText.types';
3
3
  export declare function hrefForFormattedLink(url: string): string;
4
- export declare function FormattedText({ text, className, onButtonClick, }: FormattedTextProps): React.JSX.Element;
4
+ export declare function findJsonEndExclusive(content: string, start: number): number;
5
+ export declare function FormattedText({ text, className, onButtonClick, quotes, injectors, breaks, }: FormattedTextProps): React.JSX.Element;
@@ -1,3 +1,10 @@
1
+ import type React from 'react';
2
+ export type InjectionResult = {
3
+ elem: React.ReactNode;
4
+ index: number;
5
+ length: number;
6
+ } | null;
7
+ export type Injector = (content: string) => InjectionResult;
1
8
  export type FormattedTextProps = {
2
9
  text: string;
3
10
  className?: string;
@@ -5,4 +12,7 @@ export type FormattedTextProps = {
5
12
  text: string;
6
13
  [name: string]: string;
7
14
  }) => void;
15
+ quotes?: boolean;
16
+ injectors?: Injector[];
17
+ breaks?: boolean;
8
18
  };
@@ -1,2 +1,2 @@
1
- export { FormattedText } from './FormattedText';
2
- export type { FormattedTextProps } from './FormattedText.types';
1
+ export { FormattedText, findJsonEndExclusive } from './FormattedText';
2
+ export type { FormattedTextProps, InjectionResult, Injector, } from './FormattedText.types';
@@ -0,0 +1,7 @@
1
+ import * as T from './Img.types';
2
+ export type ImgProps = T.Props;
3
+ /**
4
+ * `<img>` that never shows the browser's broken-image glyph or its alt text:
5
+ * when loading fails it renders `fallback` (a broken-image icon by default).
6
+ */
7
+ export declare function Img({ src, alt, fallback, fallbackClassName, className, onError, ...rest }: T.Props): import("react").JSX.Element;
@@ -0,0 +1 @@
1
+ import '@testing-library/jest-dom';
@@ -0,0 +1,10 @@
1
+ import type { ImgHTMLAttributes, ReactNode } from 'react';
2
+ export type Props = ImgHTMLAttributes<HTMLImageElement> & {
3
+ /**
4
+ * Rendered instead of the image when it fails to load. Defaults to a
5
+ * broken-image icon. Pass `null` to render nothing.
6
+ */
7
+ fallback?: ReactNode;
8
+ /** Class for the default fallback wrapper. */
9
+ fallbackClassName?: string;
10
+ };
@@ -0,0 +1,9 @@
1
+ import type { Editor } from '@tiptap/core';
2
+ /**
3
+ * Placeholder text for the composer's empty paragraphs. Only a wholly empty
4
+ * composer shows it: a quote or list with an empty line after it already is
5
+ * content, so it keeps no hint.
6
+ */
7
+ export declare function promptComposerPlaceholder(text: string): ({ editor }: {
8
+ editor: Editor;
9
+ }) => string;
@@ -24,6 +24,7 @@ export * from './Gallery/Gallery';
24
24
  export * from './GridLayout/GridLayout';
25
25
  export * from './Heading/Heading';
26
26
  export * from './Icon/Icon';
27
+ export * from './Img/Img';
27
28
  export * from './Input/Input';
28
29
  export * from './InputFile/InputFile';
29
30
  export * from './Label/Label';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@homecode/ui",
3
- "version": "5.20.20",
3
+ "version": "5.20.22",
4
4
  "description": "React UI components library",
5
5
  "scripts": {
6
6
  "tests": "jest",