chore: Монорепо с общими пакетами

This commit is contained in:
2026-03-04 16:31:57 +03:00
parent 8f2c799235
commit 915c56351b
420 changed files with 13403 additions and 7071 deletions

View File

@@ -0,0 +1,18 @@
import type { StorybookConfig } from "@storybook/react-vite";
const config: StorybookConfig = {
stories: ["../src/**/*.stories.@(ts|tsx)"],
addons: ["@storybook/addon-essentials"],
framework: {
name: "@storybook/react-vite",
options: {},
},
docs: {
autodocs: "tag",
},
core: {
disableTelemetry: true,
},
};
export default config;

View File

@@ -0,0 +1,19 @@
import type { Preview } from "@storybook/react";
import "antd/dist/reset.css";
import "@hublib-web/tach-typography/styles.css";
const preview: Preview = {
parameters: {
layout: "padded",
controls: {
expanded: true,
sort: "requiredFirst",
},
actions: {
argTypesRegex: "^on[A-Z].*",
},
},
};
export default preview;

View File

@@ -0,0 +1,98 @@
# @hublib-web/content-suggestions
Cross-framework content text/title renderer with support for mentions, tags and links.
## Features
- Shared parser in `core` (`findAllEntities`, `findMentions`, `findTags`, `findLinks`).
- React UI components in `react`:
- `ContentText`
- `ContentTextWithSuggestions`
- `ContentTitleWithSuggestions`
- Angular adapter in `angular` for rendering/tokenization integration.
- Depends on `@hublib-web/tach-typography` for visual consistency.
- Business logic (API requests for mentions/tags) stays in consumer application.
## Install from Git (SSH tag)
```bash
yarn add "@hublib-web/content-suggestions@git+ssh://git@github.com/ORG/REPO.git#workspace=@hublib-web/content-suggestions&tag=content-suggestions-v0.1.0"
```
`@hublib-web/tach-typography` is a peer dependency, so install a compatible tag for it as well.
## Install inside this monorepo
```bash
yarn add @hublib-web/content-suggestions
```
## Release this package
1. Bump `version` in `packages/content-suggestions/package.json`.
2. Build package artifacts:
```bash
yarn workspace @hublib-web/content-suggestions build
```
3. Commit release files:
```bash
git add packages/content-suggestions/package.json packages/content-suggestions/dist
git commit -m "release(content-suggestions): v0.1.0"
```
4. Create and push tag:
```bash
git tag -a content-suggestions-v0.1.0 -m "@hublib-web/content-suggestions v0.1.0"
git push origin main --follow-tags
```
Detailed docs:
- [Release policy](../../docs/release-policy.md)
- [Git installation](../../docs/git-installation.md)
## React usage
```tsx
import { ContentTextWithSuggestions } from "@hublib-web/content-suggestions/react";
<ContentTextWithSuggestions text={text} />;
```
With app-specific mention business logic:
```tsx
<ContentTextWithSuggestions
text={text}
renderMention={(entity) => <MyMention entity={entity} />}
renderTag={(entity) => <MyTagLink entity={entity} />}
/>;
```
By default, tags are rendered as plain styled text (not links).
## Angular usage
```ts
import { createAngularContentTokens } from "@hublib-web/content-suggestions/angular";
const tokens = createAngularContentTokens(text);
```
## Storybook (dev/design system)
Run from repository root:
```bash
yarn workspace @hublib-web/content-suggestions storybook
```
Build static Storybook:
```bash
yarn workspace @hublib-web/content-suggestions storybook:build
```

View File

@@ -0,0 +1,141 @@
'use strict';
// src/core/parser.ts
var mentionLinkRegexp = /@\[[^\]]+]\([0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}\)/g;
var parseMention = (mention) => {
const regex = /@\[([^\]]+)\]\(([\w-]{36})\)/;
const match = mention.match(regex);
if (!match) {
return null;
}
const mentionText = match[1];
const mentionId = match[2];
if (!mentionText || !mentionId) {
return null;
}
return {
mention: `@${mentionText}`,
id: mentionId
};
};
var findMentions = (text) => {
let match;
const matches = [];
while ((match = mentionLinkRegexp.exec(text)) !== null) {
const parsed = parseMention(match[0]);
const baseMention = {
start: match.index,
end: mentionLinkRegexp.lastIndex,
text: match[0],
type: "mention",
displayText: parsed?.mention ?? match[0]
};
matches.push(parsed?.id ? { ...baseMention, userId: parsed.id } : baseMention);
}
return matches;
};
var findTags = (content) => {
const regex = /#[^\s]{1,201}/g;
const results = [];
let match;
while ((match = regex.exec(content)) !== null) {
const value = match[0];
results.push({
start: match.index,
end: match.index + value.length,
text: value,
type: "tag",
tag: value.replace("#", "")
});
}
return results;
};
var findLinks = (content) => {
const regex = /\b((https?:\/\/)?(?:[\w-]+\.)+[a-z]{2,}(\/[\w\-._~:/?#[\]@!$&'()*+,;=]*)?)/gi;
const results = [];
let match;
while ((match = regex.exec(content)) !== null) {
const rawUrl = match[0];
const hasProtocol = /^https?:\/\//i.test(rawUrl);
const fullUrl = hasProtocol ? rawUrl : `https://${rawUrl}`;
results.push({
start: match.index,
end: match.index + rawUrl.length,
text: rawUrl,
url: fullUrl,
type: "link"
});
}
return results;
};
var findAllEntities = (content) => {
const mentions = findMentions(content);
const tags = findTags(content);
const links = findLinks(content);
return [...mentions, ...tags, ...links].sort((a, b) => a.start - b.start);
};
// src/angular/index.ts
var buildAngularTagHref = (entity) => {
return `/search/?query=${encodeURIComponent(entity.tag.toLowerCase())}`;
};
var createAngularContentTokens = (inputText) => {
const text = inputText ?? "";
const entities = findAllEntities(text);
let cursor = 0;
const tokens = [];
for (const entity of entities) {
if (entity.start > cursor) {
tokens.push({
kind: "text",
text: text.slice(cursor, entity.start),
start: cursor,
end: entity.start
});
}
if (entity.type === "mention") {
tokens.push({
kind: "mention",
entity
});
} else if (entity.type === "tag") {
tokens.push({
kind: "tag",
entity
});
} else {
tokens.push({
kind: "link",
entity
});
}
cursor = entity.end;
}
if (cursor < text.length) {
tokens.push({
kind: "text",
text: text.slice(cursor),
start: cursor,
end: text.length
});
}
return tokens;
};
var AngularContentSuggestionsAdapter = class {
snapshot(inputText) {
const text = inputText ?? "";
const entities = findAllEntities(text);
const tokens = createAngularContentTokens(text);
return {
text,
entities,
tokens
};
}
};
exports.AngularContentSuggestionsAdapter = AngularContentSuggestionsAdapter;
exports.buildAngularTagHref = buildAngularTagHref;
exports.createAngularContentTokens = createAngularContentTokens;
//# sourceMappingURL=index.cjs.map
//# sourceMappingURL=index.cjs.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,33 @@
import { C as ContentEntity, M as MentionEntity, T as TagEntity, L as LinkEntity } from '../types-BRt4hd7A.cjs';
interface AngularTextToken {
kind: "text";
text: string;
start: number;
end: number;
}
interface AngularMentionToken {
kind: "mention";
entity: MentionEntity;
}
interface AngularTagToken {
kind: "tag";
entity: TagEntity;
}
interface AngularLinkToken {
kind: "link";
entity: LinkEntity;
}
type AngularContentToken = AngularTextToken | AngularMentionToken | AngularTagToken | AngularLinkToken;
interface AngularContentSnapshot {
text: string;
entities: ContentEntity[];
tokens: AngularContentToken[];
}
declare const buildAngularTagHref: (entity: TagEntity) => string;
declare const createAngularContentTokens: (inputText: string | null | undefined) => AngularContentToken[];
declare class AngularContentSuggestionsAdapter {
snapshot(inputText: string | null | undefined): AngularContentSnapshot;
}
export { type AngularContentSnapshot, AngularContentSuggestionsAdapter, type AngularContentToken, type AngularLinkToken, type AngularMentionToken, type AngularTagToken, type AngularTextToken, buildAngularTagHref, createAngularContentTokens };

View File

@@ -0,0 +1,33 @@
import { C as ContentEntity, M as MentionEntity, T as TagEntity, L as LinkEntity } from '../types-BRt4hd7A.js';
interface AngularTextToken {
kind: "text";
text: string;
start: number;
end: number;
}
interface AngularMentionToken {
kind: "mention";
entity: MentionEntity;
}
interface AngularTagToken {
kind: "tag";
entity: TagEntity;
}
interface AngularLinkToken {
kind: "link";
entity: LinkEntity;
}
type AngularContentToken = AngularTextToken | AngularMentionToken | AngularTagToken | AngularLinkToken;
interface AngularContentSnapshot {
text: string;
entities: ContentEntity[];
tokens: AngularContentToken[];
}
declare const buildAngularTagHref: (entity: TagEntity) => string;
declare const createAngularContentTokens: (inputText: string | null | undefined) => AngularContentToken[];
declare class AngularContentSuggestionsAdapter {
snapshot(inputText: string | null | undefined): AngularContentSnapshot;
}
export { type AngularContentSnapshot, AngularContentSuggestionsAdapter, type AngularContentToken, type AngularLinkToken, type AngularMentionToken, type AngularTagToken, type AngularTextToken, buildAngularTagHref, createAngularContentTokens };

View File

@@ -0,0 +1,137 @@
// src/core/parser.ts
var mentionLinkRegexp = /@\[[^\]]+]\([0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}\)/g;
var parseMention = (mention) => {
const regex = /@\[([^\]]+)\]\(([\w-]{36})\)/;
const match = mention.match(regex);
if (!match) {
return null;
}
const mentionText = match[1];
const mentionId = match[2];
if (!mentionText || !mentionId) {
return null;
}
return {
mention: `@${mentionText}`,
id: mentionId
};
};
var findMentions = (text) => {
let match;
const matches = [];
while ((match = mentionLinkRegexp.exec(text)) !== null) {
const parsed = parseMention(match[0]);
const baseMention = {
start: match.index,
end: mentionLinkRegexp.lastIndex,
text: match[0],
type: "mention",
displayText: parsed?.mention ?? match[0]
};
matches.push(parsed?.id ? { ...baseMention, userId: parsed.id } : baseMention);
}
return matches;
};
var findTags = (content) => {
const regex = /#[^\s]{1,201}/g;
const results = [];
let match;
while ((match = regex.exec(content)) !== null) {
const value = match[0];
results.push({
start: match.index,
end: match.index + value.length,
text: value,
type: "tag",
tag: value.replace("#", "")
});
}
return results;
};
var findLinks = (content) => {
const regex = /\b((https?:\/\/)?(?:[\w-]+\.)+[a-z]{2,}(\/[\w\-._~:/?#[\]@!$&'()*+,;=]*)?)/gi;
const results = [];
let match;
while ((match = regex.exec(content)) !== null) {
const rawUrl = match[0];
const hasProtocol = /^https?:\/\//i.test(rawUrl);
const fullUrl = hasProtocol ? rawUrl : `https://${rawUrl}`;
results.push({
start: match.index,
end: match.index + rawUrl.length,
text: rawUrl,
url: fullUrl,
type: "link"
});
}
return results;
};
var findAllEntities = (content) => {
const mentions = findMentions(content);
const tags = findTags(content);
const links = findLinks(content);
return [...mentions, ...tags, ...links].sort((a, b) => a.start - b.start);
};
// src/angular/index.ts
var buildAngularTagHref = (entity) => {
return `/search/?query=${encodeURIComponent(entity.tag.toLowerCase())}`;
};
var createAngularContentTokens = (inputText) => {
const text = inputText ?? "";
const entities = findAllEntities(text);
let cursor = 0;
const tokens = [];
for (const entity of entities) {
if (entity.start > cursor) {
tokens.push({
kind: "text",
text: text.slice(cursor, entity.start),
start: cursor,
end: entity.start
});
}
if (entity.type === "mention") {
tokens.push({
kind: "mention",
entity
});
} else if (entity.type === "tag") {
tokens.push({
kind: "tag",
entity
});
} else {
tokens.push({
kind: "link",
entity
});
}
cursor = entity.end;
}
if (cursor < text.length) {
tokens.push({
kind: "text",
text: text.slice(cursor),
start: cursor,
end: text.length
});
}
return tokens;
};
var AngularContentSuggestionsAdapter = class {
snapshot(inputText) {
const text = inputText ?? "";
const entities = findAllEntities(text);
const tokens = createAngularContentTokens(text);
return {
text,
entities,
tokens
};
}
};
export { AngularContentSuggestionsAdapter, buildAngularTagHref, createAngularContentTokens };
//# sourceMappingURL=index.js.map
//# sourceMappingURL=index.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,97 @@
'use strict';
// src/core/parser.ts
var mentionLinkRegexp = /@\[[^\]]+]\([0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}\)/g;
var parseMention = (mention) => {
const regex = /@\[([^\]]+)\]\(([\w-]{36})\)/;
const match = mention.match(regex);
if (!match) {
return null;
}
const mentionText = match[1];
const mentionId = match[2];
if (!mentionText || !mentionId) {
return null;
}
return {
mention: `@${mentionText}`,
id: mentionId
};
};
var findMentions = (text) => {
let match;
const matches = [];
while ((match = mentionLinkRegexp.exec(text)) !== null) {
const parsed = parseMention(match[0]);
const baseMention = {
start: match.index,
end: mentionLinkRegexp.lastIndex,
text: match[0],
type: "mention",
displayText: parsed?.mention ?? match[0]
};
matches.push(parsed?.id ? { ...baseMention, userId: parsed.id } : baseMention);
}
return matches;
};
var findTags = (content) => {
const regex = /#[^\s]{1,201}/g;
const results = [];
let match;
while ((match = regex.exec(content)) !== null) {
const value = match[0];
results.push({
start: match.index,
end: match.index + value.length,
text: value,
type: "tag",
tag: value.replace("#", "")
});
}
return results;
};
var findLinks = (content) => {
const regex = /\b((https?:\/\/)?(?:[\w-]+\.)+[a-z]{2,}(\/[\w\-._~:/?#[\]@!$&'()*+,;=]*)?)/gi;
const results = [];
let match;
while ((match = regex.exec(content)) !== null) {
const rawUrl = match[0];
const hasProtocol = /^https?:\/\//i.test(rawUrl);
const fullUrl = hasProtocol ? rawUrl : `https://${rawUrl}`;
results.push({
start: match.index,
end: match.index + rawUrl.length,
text: rawUrl,
url: fullUrl,
type: "link"
});
}
return results;
};
var findAllEntities = (content) => {
const mentions = findMentions(content);
const tags = findTags(content);
const links = findLinks(content);
return [...mentions, ...tags, ...links].sort((a, b) => a.start - b.start);
};
var processContent = (content) => {
const processedText = content.replace(mentionLinkRegexp, (match) => {
const parsed = parseMention(match);
return parsed ? parsed.mention : match;
});
const tags = findTags(content).map((tag) => tag.tag);
return {
processedText,
tags
};
};
exports.findAllEntities = findAllEntities;
exports.findLinks = findLinks;
exports.findMentions = findMentions;
exports.findTags = findTags;
exports.mentionLinkRegexp = mentionLinkRegexp;
exports.parseMention = parseMention;
exports.processContent = processContent;
//# sourceMappingURL=index.cjs.map
//# sourceMappingURL=index.cjs.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,12 @@
import { C as ContentEntity, L as LinkEntity, M as MentionEntity, T as TagEntity, P as ParsedMention, a as ProcessedContent } from '../types-BRt4hd7A.cjs';
export { B as BaseEntity } from '../types-BRt4hd7A.cjs';
declare const mentionLinkRegexp: RegExp;
declare const parseMention: (mention: string) => ParsedMention | null;
declare const findMentions: (text: string) => MentionEntity[];
declare const findTags: (content: string) => TagEntity[];
declare const findLinks: (content: string) => LinkEntity[];
declare const findAllEntities: (content: string) => ContentEntity[];
declare const processContent: (content: string) => ProcessedContent;
export { ContentEntity, LinkEntity, MentionEntity, ParsedMention, ProcessedContent, TagEntity, findAllEntities, findLinks, findMentions, findTags, mentionLinkRegexp, parseMention, processContent };

View File

@@ -0,0 +1,12 @@
import { C as ContentEntity, L as LinkEntity, M as MentionEntity, T as TagEntity, P as ParsedMention, a as ProcessedContent } from '../types-BRt4hd7A.js';
export { B as BaseEntity } from '../types-BRt4hd7A.js';
declare const mentionLinkRegexp: RegExp;
declare const parseMention: (mention: string) => ParsedMention | null;
declare const findMentions: (text: string) => MentionEntity[];
declare const findTags: (content: string) => TagEntity[];
declare const findLinks: (content: string) => LinkEntity[];
declare const findAllEntities: (content: string) => ContentEntity[];
declare const processContent: (content: string) => ProcessedContent;
export { ContentEntity, LinkEntity, MentionEntity, ParsedMention, ProcessedContent, TagEntity, findAllEntities, findLinks, findMentions, findTags, mentionLinkRegexp, parseMention, processContent };

View File

@@ -0,0 +1,89 @@
// src/core/parser.ts
var mentionLinkRegexp = /@\[[^\]]+]\([0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}\)/g;
var parseMention = (mention) => {
const regex = /@\[([^\]]+)\]\(([\w-]{36})\)/;
const match = mention.match(regex);
if (!match) {
return null;
}
const mentionText = match[1];
const mentionId = match[2];
if (!mentionText || !mentionId) {
return null;
}
return {
mention: `@${mentionText}`,
id: mentionId
};
};
var findMentions = (text) => {
let match;
const matches = [];
while ((match = mentionLinkRegexp.exec(text)) !== null) {
const parsed = parseMention(match[0]);
const baseMention = {
start: match.index,
end: mentionLinkRegexp.lastIndex,
text: match[0],
type: "mention",
displayText: parsed?.mention ?? match[0]
};
matches.push(parsed?.id ? { ...baseMention, userId: parsed.id } : baseMention);
}
return matches;
};
var findTags = (content) => {
const regex = /#[^\s]{1,201}/g;
const results = [];
let match;
while ((match = regex.exec(content)) !== null) {
const value = match[0];
results.push({
start: match.index,
end: match.index + value.length,
text: value,
type: "tag",
tag: value.replace("#", "")
});
}
return results;
};
var findLinks = (content) => {
const regex = /\b((https?:\/\/)?(?:[\w-]+\.)+[a-z]{2,}(\/[\w\-._~:/?#[\]@!$&'()*+,;=]*)?)/gi;
const results = [];
let match;
while ((match = regex.exec(content)) !== null) {
const rawUrl = match[0];
const hasProtocol = /^https?:\/\//i.test(rawUrl);
const fullUrl = hasProtocol ? rawUrl : `https://${rawUrl}`;
results.push({
start: match.index,
end: match.index + rawUrl.length,
text: rawUrl,
url: fullUrl,
type: "link"
});
}
return results;
};
var findAllEntities = (content) => {
const mentions = findMentions(content);
const tags = findTags(content);
const links = findLinks(content);
return [...mentions, ...tags, ...links].sort((a, b) => a.start - b.start);
};
var processContent = (content) => {
const processedText = content.replace(mentionLinkRegexp, (match) => {
const parsed = parseMention(match);
return parsed ? parsed.mention : match;
});
const tags = findTags(content).map((tag) => tag.tag);
return {
processedText,
tags
};
};
export { findAllEntities, findLinks, findMentions, findTags, mentionLinkRegexp, parseMention, processContent };
//# sourceMappingURL=index.js.map
//# sourceMappingURL=index.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,396 @@
'use strict';
var React = require('react');
var react = require('@hublib-web/tach-typography/react');
var jsxRuntime = require('react/jsx-runtime');
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
var React__default = /*#__PURE__*/_interopDefault(React);
// src/react/components/content-text.tsx
// src/core/parser.ts
var mentionLinkRegexp = /@\[[^\]]+]\([0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}\)/g;
var parseMention = (mention) => {
const regex = /@\[([^\]]+)\]\(([\w-]{36})\)/;
const match = mention.match(regex);
if (!match) {
return null;
}
const mentionText = match[1];
const mentionId = match[2];
if (!mentionText || !mentionId) {
return null;
}
return {
mention: `@${mentionText}`,
id: mentionId
};
};
var findMentions = (text) => {
let match;
const matches = [];
while ((match = mentionLinkRegexp.exec(text)) !== null) {
const parsed = parseMention(match[0]);
const baseMention = {
start: match.index,
end: mentionLinkRegexp.lastIndex,
text: match[0],
type: "mention",
displayText: parsed?.mention ?? match[0]
};
matches.push(parsed?.id ? { ...baseMention, userId: parsed.id } : baseMention);
}
return matches;
};
var findTags = (content) => {
const regex = /#[^\s]{1,201}/g;
const results = [];
let match;
while ((match = regex.exec(content)) !== null) {
const value = match[0];
results.push({
start: match.index,
end: match.index + value.length,
text: value,
type: "tag",
tag: value.replace("#", "")
});
}
return results;
};
var findLinks = (content) => {
const regex = /\b((https?:\/\/)?(?:[\w-]+\.)+[a-z]{2,}(\/[\w\-._~:/?#[\]@!$&'()*+,;=]*)?)/gi;
const results = [];
let match;
while ((match = regex.exec(content)) !== null) {
const rawUrl = match[0];
const hasProtocol = /^https?:\/\//i.test(rawUrl);
const fullUrl = hasProtocol ? rawUrl : `https://${rawUrl}`;
results.push({
start: match.index,
end: match.index + rawUrl.length,
text: rawUrl,
url: fullUrl,
type: "link"
});
}
return results;
};
var findAllEntities = (content) => {
const mentions = findMentions(content);
const tags = findTags(content);
const links = findLinks(content);
return [...mentions, ...tags, ...links].sort((a, b) => a.start - b.start);
};
var processContent = (content) => {
const processedText = content.replace(mentionLinkRegexp, (match) => {
const parsed = parseMention(match);
return parsed ? parsed.mention : match;
});
const tags = findTags(content).map((tag) => tag.tag);
return {
processedText,
tags
};
};
var joinClassName = (...values) => values.filter(Boolean).join(" ");
var baseSelectableStyle = {
whiteSpace: "pre-wrap",
WebkitTouchCallout: "default",
WebkitUserSelect: "text",
KhtmlUserSelect: "text",
MozUserSelect: "text",
msUserSelect: "text",
userSelect: "text"
};
var blurStyle = {
filter: "blur(3px)",
WebkitUserSelect: "none",
KhtmlUserSelect: "none",
MozUserSelect: "none",
msUserSelect: "none",
userSelect: "none",
pointerEvents: "none"
};
var ReadMoreButton = ({ onClick, symbol = "\u0427\u0438\u0442\u0430\u0442\u044C \u043F\u043E\u043B\u043D\u043E\u0441\u0442\u044C\u044E", expanded = false, TitleComponent }) => /* @__PURE__ */ jsxRuntime.jsx(
TitleComponent,
{
onClick: (event) => {
event.preventDefault();
event.stopPropagation();
onClick(event);
},
weight: "bold",
level: 5,
children: typeof symbol === "function" ? symbol(expanded) : symbol
}
);
var ContentText = React__default.default.memo(
({
text,
className,
ellipsis = false,
blur = false,
weight = "normal",
style,
onView,
renderMention,
renderTag,
renderLink,
ParagraphComponent = react.TachTypography.Paragraph.Body,
TextComponent = react.TachTypography.Text.Body,
TitleComponent = react.TachTypography.Title.Body,
...props
}) => {
const containerRef = React.useRef(null);
const [expanded, setExpanded] = React.useState(false);
const [containerInsideView, setContainerInsideView] = React.useState(false);
const [viewed, setViewed] = React.useState(false);
const content = text || "";
const entities = React.useMemo(() => findAllEntities(content), [content]);
const wrapWithKey = (node, key) => {
if (React__default.default.isValidElement(node)) {
return React__default.default.cloneElement(node, { key });
}
return /* @__PURE__ */ jsxRuntime.jsx(React__default.default.Fragment, { children: node }, key);
};
const buildMentionNode = (entity, index) => {
const defaultNode = /* @__PURE__ */ jsxRuntime.jsx(TextComponent, { color: "link", weight, children: entity.displayText });
const customNode = renderMention?.(entity, index) ?? defaultNode;
return wrapWithKey(customNode, `mention-${entity.start}-${index}`);
};
const buildTagNode = (entity, index) => {
const defaultNode = /* @__PURE__ */ jsxRuntime.jsx(TextComponent, { color: "link", weight, children: entity.text });
const customNode = renderTag?.(entity, index) ?? defaultNode;
return wrapWithKey(customNode, `tag-${entity.start}-${index}`);
};
const buildLinkNode = (entity, index) => {
const defaultNode = /* @__PURE__ */ jsxRuntime.jsx(
react.TachTypography.Link.Body,
{
target: "_blank",
referrerPolicy: "no-referrer",
color: "link",
weight,
href: entity.url,
children: entity.text
}
);
const customNode = renderLink?.(entity, index) ?? defaultNode;
return wrapWithKey(customNode, `link-${entity.start}-${index}`);
};
const buildParts = (upto = null) => {
let lastIndex = 0;
const nodes = [];
for (const [i, entity] of entities.entries()) {
if (upto !== null && entity.start >= upto) {
break;
}
const textEnd = upto !== null ? Math.min(entity.start, upto) : entity.start;
if (entity.start > lastIndex && lastIndex < textEnd) {
nodes.push(content.slice(lastIndex, textEnd));
}
if (upto === null || entity.end <= upto) {
if (entity.type === "mention") {
nodes.push(buildMentionNode(entity, i));
} else if (entity.type === "tag") {
nodes.push(buildTagNode(entity, i));
} else if (entity.type === "link") {
nodes.push(buildLinkNode(entity, i));
}
}
lastIndex = entity.end;
}
if (upto === null) {
if (lastIndex < content.length) {
nodes.push(content.slice(lastIndex));
}
} else if (lastIndex < upto) {
nodes.push(content.slice(lastIndex, upto));
}
return nodes;
};
const ellipsisConfig = ellipsis && typeof ellipsis === "object" ? ellipsis : null;
const expandedFromProps = ellipsisConfig && typeof ellipsisConfig.expanded === "boolean" ? ellipsisConfig.expanded : void 0;
const isExpandedControlled = expandedFromProps !== void 0;
const mergedExpanded = isExpandedControlled ? expandedFromProps : expanded;
const handleExpand = React.useCallback(
(event) => {
if (!isExpandedControlled) {
setExpanded(true);
}
ellipsisConfig?.onExpand?.(event, { expanded: true });
},
[ellipsisConfig, isExpandedControlled]
);
React.useEffect(() => {
if (isExpandedControlled) {
return;
}
if (ellipsisConfig && "count" in ellipsisConfig) {
const count = ellipsisConfig.count ?? 0;
if (content.length <= count && !expanded) {
setExpanded(true);
}
}
}, [content.length, ellipsisConfig, expanded, isExpandedControlled]);
React.useEffect(() => {
if (mergedExpanded && !viewed && containerInsideView) {
setViewed(true);
onView?.();
}
}, [mergedExpanded, viewed, containerInsideView, onView]);
React.useEffect(() => {
const ref = containerRef.current;
if (!ref) {
return;
}
const observer = new IntersectionObserver((entries) => {
const entry = entries[0];
if (!entry) {
return;
}
if (entry.isIntersecting && !containerInsideView) {
setContainerInsideView(true);
}
}, { threshold: 0.5 });
observer.observe(ref);
return () => {
observer.unobserve(ref);
};
}, [containerInsideView]);
const mergedStyle = {
...baseSelectableStyle,
...blur ? blurStyle : void 0,
...style
};
if (ellipsisConfig && "count" in ellipsisConfig) {
const { count, expandable } = ellipsisConfig;
if (!mergedExpanded && count && content.length > count) {
let cutoff = count;
let extended = true;
while (extended) {
extended = false;
for (const entity of entities) {
if (entity.start < cutoff && entity.end > cutoff) {
cutoff = entity.end;
extended = true;
}
}
}
const truncatedNodes = buildParts(cutoff);
return /* @__PURE__ */ jsxRuntime.jsxs(
ParagraphComponent,
{
ref: containerRef,
weight,
className: joinClassName(className),
style: mergedStyle,
...props,
children: [
truncatedNodes,
"\u2026",
expandable && /* @__PURE__ */ jsxRuntime.jsx(
ReadMoreButton,
{
symbol: ellipsisConfig.symbol,
onClick: handleExpand,
expanded: mergedExpanded,
TitleComponent
}
)
]
}
);
}
}
if (ellipsisConfig && "rows" in ellipsisConfig) {
const paragraphEllipsis = mergedExpanded ? false : {
...ellipsisConfig,
symbol: ellipsisConfig.expandable ? /* @__PURE__ */ jsxRuntime.jsx(
ReadMoreButton,
{
symbol: ellipsisConfig.symbol,
onClick: handleExpand,
expanded: mergedExpanded,
TitleComponent
}
) : ellipsisConfig.symbol
};
return /* @__PURE__ */ jsxRuntime.jsx(
ParagraphComponent,
{
ref: containerRef,
weight,
className: joinClassName(className),
style: mergedStyle,
ellipsis: paragraphEllipsis,
...props,
children: buildParts()
}
);
}
return /* @__PURE__ */ jsxRuntime.jsx(
ParagraphComponent,
{
ref: containerRef,
weight,
className: joinClassName(className),
style: mergedStyle,
...props,
children: buildParts()
}
);
}
);
ContentText.displayName = "ContentText";
var DefaultMention = ({ entity }) => /* @__PURE__ */ jsxRuntime.jsx(react.TachTypography.Text.Body, { color: "link", children: entity.displayText });
var DefaultTag = ({ entity }) => /* @__PURE__ */ jsxRuntime.jsx(react.TachTypography.Text.Body, { color: "link", children: entity.text });
var ContentTextWithSuggestions = ({
renderMention,
renderTag,
...props
}) => {
return /* @__PURE__ */ jsxRuntime.jsx(
ContentText,
{
...props,
renderMention: (entity, index) => renderMention ? renderMention(entity, index) : /* @__PURE__ */ jsxRuntime.jsx(DefaultMention, { entity }),
renderTag: (entity, index) => renderTag ? renderTag(entity, index) : /* @__PURE__ */ jsxRuntime.jsx(DefaultTag, { entity })
}
);
};
var ContentTitleWithSuggestions = ({
text,
ellipsis,
blur = false,
...rest
}) => {
const normalizedEllipsis = ellipsis === void 0 ? { rows: 2 } : ellipsis;
const textProps = text === void 0 ? {} : { text };
return /* @__PURE__ */ jsxRuntime.jsx(
ContentTextWithSuggestions,
{
weight: "bold",
blur,
ellipsis: normalizedEllipsis,
...textProps,
...rest
}
);
};
exports.ContentText = ContentText;
exports.ContentTextWithSuggestions = ContentTextWithSuggestions;
exports.ContentTitleWithSuggestions = ContentTitleWithSuggestions;
exports.findAllEntities = findAllEntities;
exports.findLinks = findLinks;
exports.findMentions = findMentions;
exports.findTags = findTags;
exports.mentionLinkRegexp = mentionLinkRegexp;
exports.parseMention = parseMention;
exports.processContent = processContent;
//# sourceMappingURL=index.cjs.map
//# sourceMappingURL=index.cjs.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,48 @@
import React, { ComponentProps, ReactNode } from 'react';
import { EllipsisConfig } from 'antd/lib/typography/Base';
import { M as MentionEntity, T as TagEntity, L as LinkEntity } from '../types-BRt4hd7A.cjs';
export { B as BaseEntity, C as ContentEntity, P as ParsedMention, a as ProcessedContent } from '../types-BRt4hd7A.cjs';
import { TachTypography } from '@hublib-web/tach-typography/react';
import * as react_jsx_runtime from 'react/jsx-runtime';
export { findAllEntities, findLinks, findMentions, findTags, mentionLinkRegexp, parseMention, processContent } from '../core/index.cjs';
type CustomEllipsisConfig = ({
count: number;
rows?: never;
expandable?: boolean;
} & Partial<EllipsisConfig>) | ({
rows: number;
count?: never;
expandable?: boolean;
} & Partial<EllipsisConfig>) | false;
type ParagraphBodyProps = ComponentProps<typeof TachTypography.Paragraph.Body>;
type TextBodyProps = ComponentProps<typeof TachTypography.Text.Body>;
type TitleBodyProps = ComponentProps<typeof TachTypography.Title.Body>;
interface ContentTextProps {
className?: string;
weight?: TextBodyProps["weight"];
text?: string | null;
ellipsis?: CustomEllipsisConfig;
blur?: boolean;
style?: TextBodyProps["style"];
onView?: () => void;
renderMention?: (entity: MentionEntity, index: number) => ReactNode;
renderTag?: (entity: TagEntity, index: number) => ReactNode;
renderLink?: (entity: LinkEntity, index: number) => ReactNode;
ParagraphComponent?: React.ComponentType<ParagraphBodyProps>;
TextComponent?: React.ComponentType<TextBodyProps>;
TitleComponent?: React.ComponentType<TitleBodyProps>;
}
declare const ContentText: React.NamedExoticComponent<ContentTextProps>;
type BaseContentTextProps = Omit<ComponentProps<typeof ContentText>, "renderMention" | "renderTag"> & {
renderMention?: (entity: MentionEntity, index: number) => React.ReactNode;
renderTag?: (entity: TagEntity, index: number) => React.ReactNode;
};
declare const ContentTextWithSuggestions: ({ renderMention, renderTag, ...props }: BaseContentTextProps) => react_jsx_runtime.JSX.Element;
interface ContentTitleWithSuggestionsProps extends Omit<ComponentProps<typeof ContentTextWithSuggestions>, "weight"> {
}
declare const ContentTitleWithSuggestions: ({ text, ellipsis, blur, ...rest }: ContentTitleWithSuggestionsProps) => react_jsx_runtime.JSX.Element;
export { ContentText, type ContentTextProps, ContentTextWithSuggestions, ContentTitleWithSuggestions, LinkEntity, MentionEntity, TagEntity };

View File

@@ -0,0 +1,48 @@
import React, { ComponentProps, ReactNode } from 'react';
import { EllipsisConfig } from 'antd/lib/typography/Base';
import { M as MentionEntity, T as TagEntity, L as LinkEntity } from '../types-BRt4hd7A.js';
export { B as BaseEntity, C as ContentEntity, P as ParsedMention, a as ProcessedContent } from '../types-BRt4hd7A.js';
import { TachTypography } from '@hublib-web/tach-typography/react';
import * as react_jsx_runtime from 'react/jsx-runtime';
export { findAllEntities, findLinks, findMentions, findTags, mentionLinkRegexp, parseMention, processContent } from '../core/index.js';
type CustomEllipsisConfig = ({
count: number;
rows?: never;
expandable?: boolean;
} & Partial<EllipsisConfig>) | ({
rows: number;
count?: never;
expandable?: boolean;
} & Partial<EllipsisConfig>) | false;
type ParagraphBodyProps = ComponentProps<typeof TachTypography.Paragraph.Body>;
type TextBodyProps = ComponentProps<typeof TachTypography.Text.Body>;
type TitleBodyProps = ComponentProps<typeof TachTypography.Title.Body>;
interface ContentTextProps {
className?: string;
weight?: TextBodyProps["weight"];
text?: string | null;
ellipsis?: CustomEllipsisConfig;
blur?: boolean;
style?: TextBodyProps["style"];
onView?: () => void;
renderMention?: (entity: MentionEntity, index: number) => ReactNode;
renderTag?: (entity: TagEntity, index: number) => ReactNode;
renderLink?: (entity: LinkEntity, index: number) => ReactNode;
ParagraphComponent?: React.ComponentType<ParagraphBodyProps>;
TextComponent?: React.ComponentType<TextBodyProps>;
TitleComponent?: React.ComponentType<TitleBodyProps>;
}
declare const ContentText: React.NamedExoticComponent<ContentTextProps>;
type BaseContentTextProps = Omit<ComponentProps<typeof ContentText>, "renderMention" | "renderTag"> & {
renderMention?: (entity: MentionEntity, index: number) => React.ReactNode;
renderTag?: (entity: TagEntity, index: number) => React.ReactNode;
};
declare const ContentTextWithSuggestions: ({ renderMention, renderTag, ...props }: BaseContentTextProps) => react_jsx_runtime.JSX.Element;
interface ContentTitleWithSuggestionsProps extends Omit<ComponentProps<typeof ContentTextWithSuggestions>, "weight"> {
}
declare const ContentTitleWithSuggestions: ({ text, ellipsis, blur, ...rest }: ContentTitleWithSuggestionsProps) => react_jsx_runtime.JSX.Element;
export { ContentText, type ContentTextProps, ContentTextWithSuggestions, ContentTitleWithSuggestions, LinkEntity, MentionEntity, TagEntity };

View File

@@ -0,0 +1,381 @@
import React, { useRef, useState, useMemo, useCallback, useEffect } from 'react';
import { TachTypography } from '@hublib-web/tach-typography/react';
import { jsxs, jsx } from 'react/jsx-runtime';
// src/react/components/content-text.tsx
// src/core/parser.ts
var mentionLinkRegexp = /@\[[^\]]+]\([0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}\)/g;
var parseMention = (mention) => {
const regex = /@\[([^\]]+)\]\(([\w-]{36})\)/;
const match = mention.match(regex);
if (!match) {
return null;
}
const mentionText = match[1];
const mentionId = match[2];
if (!mentionText || !mentionId) {
return null;
}
return {
mention: `@${mentionText}`,
id: mentionId
};
};
var findMentions = (text) => {
let match;
const matches = [];
while ((match = mentionLinkRegexp.exec(text)) !== null) {
const parsed = parseMention(match[0]);
const baseMention = {
start: match.index,
end: mentionLinkRegexp.lastIndex,
text: match[0],
type: "mention",
displayText: parsed?.mention ?? match[0]
};
matches.push(parsed?.id ? { ...baseMention, userId: parsed.id } : baseMention);
}
return matches;
};
var findTags = (content) => {
const regex = /#[^\s]{1,201}/g;
const results = [];
let match;
while ((match = regex.exec(content)) !== null) {
const value = match[0];
results.push({
start: match.index,
end: match.index + value.length,
text: value,
type: "tag",
tag: value.replace("#", "")
});
}
return results;
};
var findLinks = (content) => {
const regex = /\b((https?:\/\/)?(?:[\w-]+\.)+[a-z]{2,}(\/[\w\-._~:/?#[\]@!$&'()*+,;=]*)?)/gi;
const results = [];
let match;
while ((match = regex.exec(content)) !== null) {
const rawUrl = match[0];
const hasProtocol = /^https?:\/\//i.test(rawUrl);
const fullUrl = hasProtocol ? rawUrl : `https://${rawUrl}`;
results.push({
start: match.index,
end: match.index + rawUrl.length,
text: rawUrl,
url: fullUrl,
type: "link"
});
}
return results;
};
var findAllEntities = (content) => {
const mentions = findMentions(content);
const tags = findTags(content);
const links = findLinks(content);
return [...mentions, ...tags, ...links].sort((a, b) => a.start - b.start);
};
var processContent = (content) => {
const processedText = content.replace(mentionLinkRegexp, (match) => {
const parsed = parseMention(match);
return parsed ? parsed.mention : match;
});
const tags = findTags(content).map((tag) => tag.tag);
return {
processedText,
tags
};
};
var joinClassName = (...values) => values.filter(Boolean).join(" ");
var baseSelectableStyle = {
whiteSpace: "pre-wrap",
WebkitTouchCallout: "default",
WebkitUserSelect: "text",
KhtmlUserSelect: "text",
MozUserSelect: "text",
msUserSelect: "text",
userSelect: "text"
};
var blurStyle = {
filter: "blur(3px)",
WebkitUserSelect: "none",
KhtmlUserSelect: "none",
MozUserSelect: "none",
msUserSelect: "none",
userSelect: "none",
pointerEvents: "none"
};
var ReadMoreButton = ({ onClick, symbol = "\u0427\u0438\u0442\u0430\u0442\u044C \u043F\u043E\u043B\u043D\u043E\u0441\u0442\u044C\u044E", expanded = false, TitleComponent }) => /* @__PURE__ */ jsx(
TitleComponent,
{
onClick: (event) => {
event.preventDefault();
event.stopPropagation();
onClick(event);
},
weight: "bold",
level: 5,
children: typeof symbol === "function" ? symbol(expanded) : symbol
}
);
var ContentText = React.memo(
({
text,
className,
ellipsis = false,
blur = false,
weight = "normal",
style,
onView,
renderMention,
renderTag,
renderLink,
ParagraphComponent = TachTypography.Paragraph.Body,
TextComponent = TachTypography.Text.Body,
TitleComponent = TachTypography.Title.Body,
...props
}) => {
const containerRef = useRef(null);
const [expanded, setExpanded] = useState(false);
const [containerInsideView, setContainerInsideView] = useState(false);
const [viewed, setViewed] = useState(false);
const content = text || "";
const entities = useMemo(() => findAllEntities(content), [content]);
const wrapWithKey = (node, key) => {
if (React.isValidElement(node)) {
return React.cloneElement(node, { key });
}
return /* @__PURE__ */ jsx(React.Fragment, { children: node }, key);
};
const buildMentionNode = (entity, index) => {
const defaultNode = /* @__PURE__ */ jsx(TextComponent, { color: "link", weight, children: entity.displayText });
const customNode = renderMention?.(entity, index) ?? defaultNode;
return wrapWithKey(customNode, `mention-${entity.start}-${index}`);
};
const buildTagNode = (entity, index) => {
const defaultNode = /* @__PURE__ */ jsx(TextComponent, { color: "link", weight, children: entity.text });
const customNode = renderTag?.(entity, index) ?? defaultNode;
return wrapWithKey(customNode, `tag-${entity.start}-${index}`);
};
const buildLinkNode = (entity, index) => {
const defaultNode = /* @__PURE__ */ jsx(
TachTypography.Link.Body,
{
target: "_blank",
referrerPolicy: "no-referrer",
color: "link",
weight,
href: entity.url,
children: entity.text
}
);
const customNode = renderLink?.(entity, index) ?? defaultNode;
return wrapWithKey(customNode, `link-${entity.start}-${index}`);
};
const buildParts = (upto = null) => {
let lastIndex = 0;
const nodes = [];
for (const [i, entity] of entities.entries()) {
if (upto !== null && entity.start >= upto) {
break;
}
const textEnd = upto !== null ? Math.min(entity.start, upto) : entity.start;
if (entity.start > lastIndex && lastIndex < textEnd) {
nodes.push(content.slice(lastIndex, textEnd));
}
if (upto === null || entity.end <= upto) {
if (entity.type === "mention") {
nodes.push(buildMentionNode(entity, i));
} else if (entity.type === "tag") {
nodes.push(buildTagNode(entity, i));
} else if (entity.type === "link") {
nodes.push(buildLinkNode(entity, i));
}
}
lastIndex = entity.end;
}
if (upto === null) {
if (lastIndex < content.length) {
nodes.push(content.slice(lastIndex));
}
} else if (lastIndex < upto) {
nodes.push(content.slice(lastIndex, upto));
}
return nodes;
};
const ellipsisConfig = ellipsis && typeof ellipsis === "object" ? ellipsis : null;
const expandedFromProps = ellipsisConfig && typeof ellipsisConfig.expanded === "boolean" ? ellipsisConfig.expanded : void 0;
const isExpandedControlled = expandedFromProps !== void 0;
const mergedExpanded = isExpandedControlled ? expandedFromProps : expanded;
const handleExpand = useCallback(
(event) => {
if (!isExpandedControlled) {
setExpanded(true);
}
ellipsisConfig?.onExpand?.(event, { expanded: true });
},
[ellipsisConfig, isExpandedControlled]
);
useEffect(() => {
if (isExpandedControlled) {
return;
}
if (ellipsisConfig && "count" in ellipsisConfig) {
const count = ellipsisConfig.count ?? 0;
if (content.length <= count && !expanded) {
setExpanded(true);
}
}
}, [content.length, ellipsisConfig, expanded, isExpandedControlled]);
useEffect(() => {
if (mergedExpanded && !viewed && containerInsideView) {
setViewed(true);
onView?.();
}
}, [mergedExpanded, viewed, containerInsideView, onView]);
useEffect(() => {
const ref = containerRef.current;
if (!ref) {
return;
}
const observer = new IntersectionObserver((entries) => {
const entry = entries[0];
if (!entry) {
return;
}
if (entry.isIntersecting && !containerInsideView) {
setContainerInsideView(true);
}
}, { threshold: 0.5 });
observer.observe(ref);
return () => {
observer.unobserve(ref);
};
}, [containerInsideView]);
const mergedStyle = {
...baseSelectableStyle,
...blur ? blurStyle : void 0,
...style
};
if (ellipsisConfig && "count" in ellipsisConfig) {
const { count, expandable } = ellipsisConfig;
if (!mergedExpanded && count && content.length > count) {
let cutoff = count;
let extended = true;
while (extended) {
extended = false;
for (const entity of entities) {
if (entity.start < cutoff && entity.end > cutoff) {
cutoff = entity.end;
extended = true;
}
}
}
const truncatedNodes = buildParts(cutoff);
return /* @__PURE__ */ jsxs(
ParagraphComponent,
{
ref: containerRef,
weight,
className: joinClassName(className),
style: mergedStyle,
...props,
children: [
truncatedNodes,
"\u2026",
expandable && /* @__PURE__ */ jsx(
ReadMoreButton,
{
symbol: ellipsisConfig.symbol,
onClick: handleExpand,
expanded: mergedExpanded,
TitleComponent
}
)
]
}
);
}
}
if (ellipsisConfig && "rows" in ellipsisConfig) {
const paragraphEllipsis = mergedExpanded ? false : {
...ellipsisConfig,
symbol: ellipsisConfig.expandable ? /* @__PURE__ */ jsx(
ReadMoreButton,
{
symbol: ellipsisConfig.symbol,
onClick: handleExpand,
expanded: mergedExpanded,
TitleComponent
}
) : ellipsisConfig.symbol
};
return /* @__PURE__ */ jsx(
ParagraphComponent,
{
ref: containerRef,
weight,
className: joinClassName(className),
style: mergedStyle,
ellipsis: paragraphEllipsis,
...props,
children: buildParts()
}
);
}
return /* @__PURE__ */ jsx(
ParagraphComponent,
{
ref: containerRef,
weight,
className: joinClassName(className),
style: mergedStyle,
...props,
children: buildParts()
}
);
}
);
ContentText.displayName = "ContentText";
var DefaultMention = ({ entity }) => /* @__PURE__ */ jsx(TachTypography.Text.Body, { color: "link", children: entity.displayText });
var DefaultTag = ({ entity }) => /* @__PURE__ */ jsx(TachTypography.Text.Body, { color: "link", children: entity.text });
var ContentTextWithSuggestions = ({
renderMention,
renderTag,
...props
}) => {
return /* @__PURE__ */ jsx(
ContentText,
{
...props,
renderMention: (entity, index) => renderMention ? renderMention(entity, index) : /* @__PURE__ */ jsx(DefaultMention, { entity }),
renderTag: (entity, index) => renderTag ? renderTag(entity, index) : /* @__PURE__ */ jsx(DefaultTag, { entity })
}
);
};
var ContentTitleWithSuggestions = ({
text,
ellipsis,
blur = false,
...rest
}) => {
const normalizedEllipsis = ellipsis === void 0 ? { rows: 2 } : ellipsis;
const textProps = text === void 0 ? {} : { text };
return /* @__PURE__ */ jsx(
ContentTextWithSuggestions,
{
weight: "bold",
blur,
ellipsis: normalizedEllipsis,
...textProps,
...rest
}
);
};
export { ContentText, ContentTextWithSuggestions, ContentTitleWithSuggestions, findAllEntities, findLinks, findMentions, findTags, mentionLinkRegexp, parseMention, processContent };
//# sourceMappingURL=index.js.map
//# sourceMappingURL=index.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,29 @@
interface BaseEntity {
start: number;
end: number;
text: string;
}
interface MentionEntity extends BaseEntity {
type: "mention";
displayText: string;
userId?: string;
}
interface TagEntity extends BaseEntity {
type: "tag";
tag: string;
}
interface LinkEntity extends BaseEntity {
type: "link";
url: string;
}
type ContentEntity = MentionEntity | TagEntity | LinkEntity;
interface ParsedMention {
mention: string;
id: string;
}
interface ProcessedContent {
processedText: string;
tags: string[];
}
export type { BaseEntity as B, ContentEntity as C, LinkEntity as L, MentionEntity as M, ParsedMention as P, TagEntity as T, ProcessedContent as a };

View File

@@ -0,0 +1,29 @@
interface BaseEntity {
start: number;
end: number;
text: string;
}
interface MentionEntity extends BaseEntity {
type: "mention";
displayText: string;
userId?: string;
}
interface TagEntity extends BaseEntity {
type: "tag";
tag: string;
}
interface LinkEntity extends BaseEntity {
type: "link";
url: string;
}
type ContentEntity = MentionEntity | TagEntity | LinkEntity;
interface ParsedMention {
mention: string;
id: string;
}
interface ProcessedContent {
processedText: string;
tags: string[];
}
export type { BaseEntity as B, ContentEntity as C, LinkEntity as L, MentionEntity as M, ParsedMention as P, TagEntity as T, ProcessedContent as a };

View File

@@ -0,0 +1,105 @@
{
"name": "@hublib-web/content-suggestions",
"version": "0.1.0",
"description": "Content text/title with mentions, tags and links for React and Angular",
"license": "MIT",
"type": "module",
"main": "./dist/core/index.cjs",
"module": "./dist/core/index.js",
"types": "./dist/core/index.d.ts",
"sideEffects": false,
"files": [
"dist",
"README.md"
],
"typesVersions": {
"*": {
"react": [
"dist/react/index.d.ts"
],
"angular": [
"dist/angular/index.d.ts"
],
"core": [
"dist/core/index.d.ts"
]
}
},
"exports": {
".": {
"types": "./dist/core/index.d.ts",
"import": "./dist/core/index.js",
"require": "./dist/core/index.cjs"
},
"./core": {
"types": "./dist/core/index.d.ts",
"import": "./dist/core/index.js",
"require": "./dist/core/index.cjs"
},
"./react": {
"types": "./dist/react/index.d.ts",
"import": "./dist/react/index.js",
"require": "./dist/react/index.cjs"
},
"./angular": {
"types": "./dist/angular/index.d.ts",
"import": "./dist/angular/index.js",
"require": "./dist/angular/index.cjs"
}
},
"scripts": {
"build": "yarn clean && tsup",
"clean": "rm -rf dist storybook-static",
"typecheck": "tsc -p tsconfig.json --noEmit",
"test": "vitest run --passWithNoTests",
"lint": "eslint src --ext .ts,.tsx",
"storybook": "storybook dev -p 6006",
"storybook:build": "storybook build"
},
"peerDependencies": {
"@angular/common": ">=17.0.0",
"@angular/core": ">=17.0.0",
"@hublib-web/tach-typography": ">=0.1.0",
"antd": ">=5.0.0",
"react": ">=18.0.0",
"react-dom": ">=18.0.0",
"rxjs": ">=7.0.0"
},
"peerDependenciesMeta": {
"@angular/common": {
"optional": true
},
"@angular/core": {
"optional": true
},
"antd": {
"optional": true
},
"react": {
"optional": true
},
"react-dom": {
"optional": true
},
"rxjs": {
"optional": true
}
},
"devDependencies": {
"@angular/common": "^20.3.17",
"@angular/core": "^20.3.17",
"@hublib-web/tach-typography": "workspace:*",
"@storybook/addon-essentials": "8.6.14",
"@storybook/react": "8.6.14",
"@storybook/react-vite": "8.6.14",
"@types/react": "^19.2.2",
"antd": "^5.29.3",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"rxjs": "^7.8.2",
"storybook": "8.6.14",
"tsup": "^8.5.0",
"typescript": "^5.9.3",
"vite": "6.4.1"
}
}

View File

@@ -0,0 +1,106 @@
import type { ContentEntity, LinkEntity, MentionEntity, TagEntity } from "../core";
import { findAllEntities } from "../core";
export interface AngularTextToken {
kind: "text";
text: string;
start: number;
end: number;
}
export interface AngularMentionToken {
kind: "mention";
entity: MentionEntity;
}
export interface AngularTagToken {
kind: "tag";
entity: TagEntity;
}
export interface AngularLinkToken {
kind: "link";
entity: LinkEntity;
}
export type AngularContentToken =
| AngularTextToken
| AngularMentionToken
| AngularTagToken
| AngularLinkToken;
export interface AngularContentSnapshot {
text: string;
entities: ContentEntity[];
tokens: AngularContentToken[];
}
export const buildAngularTagHref = (entity: TagEntity): string => {
return `/search/?query=${encodeURIComponent(entity.tag.toLowerCase())}`;
};
export const createAngularContentTokens = (
inputText: string | null | undefined,
): AngularContentToken[] => {
const text = inputText ?? "";
const entities = findAllEntities(text);
let cursor = 0;
const tokens: AngularContentToken[] = [];
for (const entity of entities) {
if (entity.start > cursor) {
tokens.push({
kind: "text",
text: text.slice(cursor, entity.start),
start: cursor,
end: entity.start,
});
}
if (entity.type === "mention") {
tokens.push({
kind: "mention",
entity,
});
} else if (entity.type === "tag") {
tokens.push({
kind: "tag",
entity,
});
} else {
tokens.push({
kind: "link",
entity,
});
}
cursor = entity.end;
}
if (cursor < text.length) {
tokens.push({
kind: "text",
text: text.slice(cursor),
start: cursor,
end: text.length,
});
}
return tokens;
};
export class AngularContentSuggestionsAdapter {
snapshot(inputText: string | null | undefined): AngularContentSnapshot {
const text = inputText ?? "";
const entities = findAllEntities(text);
const tokens = createAngularContentTokens(text);
return {
text,
entities,
tokens,
};
}
}

View File

@@ -0,0 +1,19 @@
export type {
BaseEntity,
ContentEntity,
LinkEntity,
MentionEntity,
ParsedMention,
ProcessedContent,
TagEntity,
} from "./types";
export {
findAllEntities,
findLinks,
findMentions,
findTags,
mentionLinkRegexp,
parseMention,
processContent,
} from "./parser";

View File

@@ -0,0 +1,115 @@
import type {
ContentEntity,
LinkEntity,
MentionEntity,
ParsedMention,
ProcessedContent,
TagEntity,
} from "./types";
export const mentionLinkRegexp =
/@\[[^\]]+]\([0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}\)/g;
export const parseMention = (mention: string): ParsedMention | null => {
const regex = /@\[([^\]]+)\]\(([\w-]{36})\)/;
const match = mention.match(regex);
if (!match) {
return null;
}
const mentionText = match[1];
const mentionId = match[2];
if (!mentionText || !mentionId) {
return null;
}
return {
mention: `@${mentionText}`,
id: mentionId,
};
};
export const findMentions = (text: string): MentionEntity[] => {
let match: RegExpExecArray | null;
const matches: MentionEntity[] = [];
while ((match = mentionLinkRegexp.exec(text)) !== null) {
const parsed = parseMention(match[0]);
const baseMention: Omit<MentionEntity, "userId"> = {
start: match.index,
end: mentionLinkRegexp.lastIndex,
text: match[0],
type: "mention",
displayText: parsed?.mention ?? match[0],
};
matches.push(parsed?.id ? { ...baseMention, userId: parsed.id } : baseMention);
}
return matches;
};
export const findTags = (content: string): TagEntity[] => {
const regex = /#[^\s]{1,201}/g;
const results: TagEntity[] = [];
let match: RegExpExecArray | null;
while ((match = regex.exec(content)) !== null) {
const value = match[0];
results.push({
start: match.index,
end: match.index + value.length,
text: value,
type: "tag",
tag: value.replace("#", ""),
});
}
return results;
};
export const findLinks = (content: string): LinkEntity[] => {
const regex =
/\b((https?:\/\/)?(?:[\w-]+\.)+[a-z]{2,}(\/[\w\-._~:/?#[\]@!$&'()*+,;=]*)?)/gi;
const results: LinkEntity[] = [];
let match: RegExpExecArray | null;
while ((match = regex.exec(content)) !== null) {
const rawUrl = match[0];
const hasProtocol = /^https?:\/\//i.test(rawUrl);
const fullUrl = hasProtocol ? rawUrl : `https://${rawUrl}`;
results.push({
start: match.index,
end: match.index + rawUrl.length,
text: rawUrl,
url: fullUrl,
type: "link",
});
}
return results;
};
export const findAllEntities = (content: string): ContentEntity[] => {
const mentions = findMentions(content);
const tags = findTags(content);
const links = findLinks(content);
return [...mentions, ...tags, ...links].sort((a, b) => a.start - b.start);
};
export const processContent = (content: string): ProcessedContent => {
const processedText = content.replace(mentionLinkRegexp, match => {
const parsed = parseMention(match);
return parsed ? parsed.mention : match;
});
const tags = findTags(content).map(tag => tag.tag);
return {
processedText,
tags,
};
};

View File

@@ -0,0 +1,33 @@
export interface BaseEntity {
start: number;
end: number;
text: string;
}
export interface MentionEntity extends BaseEntity {
type: "mention";
displayText: string;
userId?: string;
}
export interface TagEntity extends BaseEntity {
type: "tag";
tag: string;
}
export interface LinkEntity extends BaseEntity {
type: "link";
url: string;
}
export type ContentEntity = MentionEntity | TagEntity | LinkEntity;
export interface ParsedMention {
mention: string;
id: string;
}
export interface ProcessedContent {
processedText: string;
tags: string[];
}

View File

@@ -0,0 +1,47 @@
import type { ComponentProps } from "react";
import type { MentionEntity, TagEntity } from "../../core";
import React from "react";
import { TachTypography } from "@hublib-web/tach-typography/react";
import { ContentText } from "./content-text";
type BaseContentTextProps = Omit<
ComponentProps<typeof ContentText>,
"renderMention" | "renderTag"
> & {
renderMention?: (entity: MentionEntity, index: number) => React.ReactNode;
renderTag?: (entity: TagEntity, index: number) => React.ReactNode;
};
const DefaultMention = ({ entity }: { entity: MentionEntity }) => (
<TachTypography.Text.Body color="link">{entity.displayText}</TachTypography.Text.Body>
);
const DefaultTag = ({ entity }: { entity: TagEntity }) => (
<TachTypography.Text.Body color="link">
{entity.text}
</TachTypography.Text.Body>
);
export const ContentTextWithSuggestions = ({
renderMention,
renderTag,
...props
}: BaseContentTextProps) => {
return (
<ContentText
{...props}
renderMention={(entity, index) =>
renderMention ? renderMention(entity, index) : <DefaultMention entity={entity} />
}
renderTag={(entity, index) =>
renderTag ? (
renderTag(entity, index)
) : (
<DefaultTag entity={entity} />
)
}
/>
);
};

View File

@@ -0,0 +1,353 @@
import type { ComponentProps, CSSProperties, ReactNode } from "react";
import type { EllipsisConfig } from "antd/lib/typography/Base";
import type {
ContentEntity,
LinkEntity,
MentionEntity,
TagEntity,
} from "../../core";
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { TachTypography } from "@hublib-web/tach-typography/react";
import { findAllEntities } from "../../core";
type CustomEllipsisConfig =
| ({
count: number;
rows?: never;
expandable?: boolean;
} & Partial<EllipsisConfig>)
| ({
rows: number;
count?: never;
expandable?: boolean;
} & Partial<EllipsisConfig>)
| false;
type ParagraphBodyProps = ComponentProps<typeof TachTypography.Paragraph.Body>;
type TextBodyProps = ComponentProps<typeof TachTypography.Text.Body>;
type TitleBodyProps = ComponentProps<typeof TachTypography.Title.Body>;
const joinClassName = (...values: Array<string | false | undefined>) =>
values.filter(Boolean).join(" ");
const baseSelectableStyle: CSSProperties = {
whiteSpace: "pre-wrap",
WebkitTouchCallout: "default",
WebkitUserSelect: "text",
KhtmlUserSelect: "text",
MozUserSelect: "text",
msUserSelect: "text",
userSelect: "text",
};
const blurStyle: CSSProperties = {
filter: "blur(3px)",
WebkitUserSelect: "none",
KhtmlUserSelect: "none",
MozUserSelect: "none",
msUserSelect: "none",
userSelect: "none",
pointerEvents: "none",
};
const ReadMoreButton: React.FC<{
onClick: (event: React.MouseEvent<HTMLElement, MouseEvent>) => void;
symbol?: EllipsisConfig["symbol"];
expanded?: boolean;
TitleComponent: React.ComponentType<TitleBodyProps>;
}> = ({ onClick, symbol = "Читать полностью", expanded = false, TitleComponent }) => (
<TitleComponent
onClick={event => {
event.preventDefault();
event.stopPropagation();
onClick(event);
}}
weight="bold"
level={5}
>
{typeof symbol === "function" ? symbol(expanded) : symbol}
</TitleComponent>
);
export interface ContentTextProps {
className?: string;
weight?: TextBodyProps["weight"];
text?: string | null;
ellipsis?: CustomEllipsisConfig;
blur?: boolean;
style?: TextBodyProps["style"];
onView?: () => void;
renderMention?: (entity: MentionEntity, index: number) => ReactNode;
renderTag?: (entity: TagEntity, index: number) => ReactNode;
renderLink?: (entity: LinkEntity, index: number) => ReactNode;
ParagraphComponent?: React.ComponentType<ParagraphBodyProps>;
TextComponent?: React.ComponentType<TextBodyProps>;
TitleComponent?: React.ComponentType<TitleBodyProps>;
}
export const ContentText: React.NamedExoticComponent<ContentTextProps> = React.memo(
({
text,
className,
ellipsis = false,
blur = false,
weight = "normal",
style,
onView,
renderMention,
renderTag,
renderLink,
ParagraphComponent = TachTypography.Paragraph.Body,
TextComponent = TachTypography.Text.Body,
TitleComponent = TachTypography.Title.Body,
...props
}: ContentTextProps) => {
const containerRef = useRef<HTMLDivElement | null>(null);
const [expanded, setExpanded] = useState(false);
const [containerInsideView, setContainerInsideView] = useState(false);
const [viewed, setViewed] = useState(false);
const content = text || "";
const entities = useMemo<ContentEntity[]>(() => findAllEntities(content), [content]);
const wrapWithKey = (node: ReactNode, key: React.Key) => {
if (React.isValidElement(node)) {
return React.cloneElement(node, { key });
}
return <React.Fragment key={key}>{node}</React.Fragment>;
};
const buildMentionNode = (entity: MentionEntity, index: number) => {
const defaultNode = (
<TextComponent color="link" weight={weight}>
{entity.displayText}
</TextComponent>
);
const customNode = renderMention?.(entity, index) ?? defaultNode;
return wrapWithKey(customNode, `mention-${entity.start}-${index}`);
};
const buildTagNode = (entity: TagEntity, index: number) => {
const defaultNode = (
<TextComponent color="link" weight={weight}>
{entity.text}
</TextComponent>
);
const customNode = renderTag?.(entity, index) ?? defaultNode;
return wrapWithKey(customNode, `tag-${entity.start}-${index}`);
};
const buildLinkNode = (entity: LinkEntity, index: number) => {
const defaultNode = (
<TachTypography.Link.Body
target="_blank"
referrerPolicy="no-referrer"
color="link"
weight={weight}
href={entity.url}
>
{entity.text}
</TachTypography.Link.Body>
);
const customNode = renderLink?.(entity, index) ?? defaultNode;
return wrapWithKey(customNode, `link-${entity.start}-${index}`);
};
const buildParts = (upto: number | null = null): ReactNode[] => {
let lastIndex = 0;
const nodes: ReactNode[] = [];
for (const [i, entity] of entities.entries()) {
if (upto !== null && entity.start >= upto) {
break;
}
const textEnd = upto !== null ? Math.min(entity.start, upto) : entity.start;
if (entity.start > lastIndex && lastIndex < textEnd) {
nodes.push(content.slice(lastIndex, textEnd));
}
if (upto === null || entity.end <= upto) {
if (entity.type === "mention") {
nodes.push(buildMentionNode(entity, i));
} else if (entity.type === "tag") {
nodes.push(buildTagNode(entity, i));
} else if (entity.type === "link") {
nodes.push(buildLinkNode(entity, i));
}
}
lastIndex = entity.end;
}
if (upto === null) {
if (lastIndex < content.length) {
nodes.push(content.slice(lastIndex));
}
} else if (lastIndex < upto) {
nodes.push(content.slice(lastIndex, upto));
}
return nodes;
};
const ellipsisConfig = ellipsis && typeof ellipsis === "object" ? ellipsis : null;
const expandedFromProps =
ellipsisConfig && typeof ellipsisConfig.expanded === "boolean"
? ellipsisConfig.expanded
: undefined;
const isExpandedControlled = expandedFromProps !== undefined;
const mergedExpanded = isExpandedControlled ? expandedFromProps : expanded;
const handleExpand = useCallback(
(event: React.MouseEvent<HTMLElement, MouseEvent>) => {
if (!isExpandedControlled) {
setExpanded(true);
}
ellipsisConfig?.onExpand?.(event, { expanded: true });
},
[ellipsisConfig, isExpandedControlled],
);
useEffect(() => {
if (isExpandedControlled) {
return;
}
if (ellipsisConfig && "count" in ellipsisConfig) {
const count = ellipsisConfig.count ?? 0;
if (content.length <= count && !expanded) {
setExpanded(true);
}
}
}, [content.length, ellipsisConfig, expanded, isExpandedControlled]);
useEffect(() => {
if (mergedExpanded && !viewed && containerInsideView) {
setViewed(true);
onView?.();
}
}, [mergedExpanded, viewed, containerInsideView, onView]);
useEffect(() => {
const ref = containerRef.current;
if (!ref) {
return;
}
const observer = new IntersectionObserver((entries) => {
const entry = entries[0];
if (!entry) {
return;
}
if (entry.isIntersecting && !containerInsideView) {
setContainerInsideView(true);
}
}, { threshold: 0.5 });
observer.observe(ref);
return () => {
observer.unobserve(ref);
};
}, [containerInsideView]);
const mergedStyle: CSSProperties = {
...baseSelectableStyle,
...(blur ? blurStyle : undefined),
...(style as CSSProperties | undefined),
};
if (ellipsisConfig && "count" in ellipsisConfig) {
const { count, expandable } = ellipsisConfig;
if (!mergedExpanded && count && content.length > count) {
let cutoff = count;
let extended = true;
while (extended) {
extended = false;
for (const entity of entities) {
if (entity.start < cutoff && entity.end > cutoff) {
cutoff = entity.end;
extended = true;
}
}
}
const truncatedNodes = buildParts(cutoff);
return (
<ParagraphComponent
ref={containerRef}
weight={weight}
className={joinClassName(className)}
style={mergedStyle}
{...(props as ParagraphBodyProps)}
>
{truncatedNodes}
{expandable && (
<ReadMoreButton
symbol={ellipsisConfig.symbol}
onClick={handleExpand}
expanded={mergedExpanded}
TitleComponent={TitleComponent}
/>
)}
</ParagraphComponent>
);
}
}
if (ellipsisConfig && "rows" in ellipsisConfig) {
const paragraphEllipsis = mergedExpanded
? false
: {
...ellipsisConfig,
symbol: ellipsisConfig.expandable ? (
<ReadMoreButton
symbol={ellipsisConfig.symbol}
onClick={handleExpand}
expanded={mergedExpanded}
TitleComponent={TitleComponent}
/>
) : (
ellipsisConfig.symbol
),
};
return (
<ParagraphComponent
ref={containerRef}
weight={weight}
className={joinClassName(className)}
style={mergedStyle}
ellipsis={paragraphEllipsis}
{...(props as ParagraphBodyProps)}
>
{buildParts()}
</ParagraphComponent>
);
}
return (
<ParagraphComponent
ref={containerRef}
weight={weight}
className={joinClassName(className)}
style={mergedStyle}
{...(props as ParagraphBodyProps)}
>
{buildParts()}
</ParagraphComponent>
);
},
);
ContentText.displayName = "ContentText";

View File

@@ -0,0 +1,28 @@
import type { ComponentProps } from "react";
import React from "react";
import { ContentTextWithSuggestions } from "./content-text-with-suggestions";
interface ContentTitleWithSuggestionsProps
extends Omit<ComponentProps<typeof ContentTextWithSuggestions>, "weight"> {}
export const ContentTitleWithSuggestions = ({
text,
ellipsis,
blur = false,
...rest
}: ContentTitleWithSuggestionsProps) => {
const normalizedEllipsis = ellipsis === undefined ? { rows: 2 } : ellipsis;
const textProps = text === undefined ? {} : { text };
return (
<ContentTextWithSuggestions
weight="bold"
blur={blur}
ellipsis={normalizedEllipsis}
{...textProps}
{...rest}
/>
);
};

View File

@@ -0,0 +1,27 @@
export { ContentText, type ContentTextProps } from "./components/content-text";
export {
ContentTextWithSuggestions,
} from "./components/content-text-with-suggestions";
export {
ContentTitleWithSuggestions,
} from "./components/content-title-with-suggestions";
export type {
BaseEntity,
ContentEntity,
LinkEntity,
MentionEntity,
ParsedMention,
ProcessedContent,
TagEntity,
} from "../core";
export {
findAllEntities,
findLinks,
findMentions,
findTags,
mentionLinkRegexp,
parseMention,
processContent,
} from "../core";

View File

@@ -0,0 +1,77 @@
import type { Meta, StoryObj } from "@storybook/react";
import type { MentionEntity, TagEntity } from "../core";
import React from "react";
import { TachTypography } from "@hublib-web/tach-typography/react";
import { ContentTextWithSuggestions } from "../react";
const DEMO_TEXT =
"Пример текста с упоминанием @[Иван Петров](123e4567-e89b-12d3-a456-426614174000), тегом #frontend и ссылкой docs.example.com";
const meta: Meta<typeof ContentTextWithSuggestions> = {
title: "React/ContentTextWithSuggestions",
component: ContentTextWithSuggestions,
tags: ["autodocs"],
args: {
text: DEMO_TEXT,
blur: false,
},
argTypes: {
text: { control: "text" },
blur: { control: "boolean" },
ellipsis: { control: false },
renderMention: { control: false },
renderTag: { control: false },
renderLink: { control: false },
ParagraphComponent: { control: false },
TextComponent: { control: false },
TitleComponent: { control: false },
},
render: args => (
<div style={{ maxWidth: 780, width: "100%" }}>
<ContentTextWithSuggestions {...args} />
</div>
),
};
export default meta;
type Story = StoryObj<typeof meta>;
export const Playground: Story = {};
export const WithExpandableEllipsis: Story = {
args: {
ellipsis: {
rows: 2,
expandable: true,
symbol: "Читать полностью",
},
},
};
export const WithCustomMentionAndTagRender: Story = {
render: args => {
const renderMention = (entity: MentionEntity) => (
<TachTypography.Link.Body href={`https://example.com/users/${entity.userId ?? "unknown"}`} color="link">
{entity.displayText}
</TachTypography.Link.Body>
);
const renderTag = (entity: TagEntity) => (
<TachTypography.Link.Body href={`https://example.com/tags/${entity.tag}`} color="link">
{entity.text}
</TachTypography.Link.Body>
);
return (
<div style={{ maxWidth: 780, width: "100%" }}>
<ContentTextWithSuggestions
{...args}
renderMention={renderMention}
renderTag={renderTag}
/>
</div>
);
},
};

View File

@@ -0,0 +1,50 @@
import type { Meta, StoryObj } from "@storybook/react";
import React from "react";
import { ContentTitleWithSuggestions } from "../react";
const DEMO_TITLE =
"Заголовок с @[Мария](123e4567-e89b-12d3-a456-426614174001), тегом #release и ссылкой github.com";
const meta: Meta<typeof ContentTitleWithSuggestions> = {
title: "React/ContentTitleWithSuggestions",
component: ContentTitleWithSuggestions,
tags: ["autodocs"],
args: {
text: DEMO_TITLE,
blur: false,
ellipsis: {
rows: 2,
expandable: true,
symbol: "Читать полностью",
},
},
argTypes: {
text: { control: "text" },
blur: { control: "boolean" },
ellipsis: { control: false },
renderMention: { control: false },
renderTag: { control: false },
renderLink: { control: false },
ParagraphComponent: { control: false },
TextComponent: { control: false },
TitleComponent: { control: false },
},
render: args => (
<div style={{ maxWidth: 780, width: "100%" }}>
<ContentTitleWithSuggestions {...args} />
</div>
),
};
export default meta;
type Story = StoryObj<typeof meta>;
export const Playground: Story = {};
export const Blurred: Story = {
args: {
blur: true,
},
};

View File

@@ -0,0 +1,10 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"rootDir": "src",
"outDir": "dist",
"types": ["node", "react"]
},
"include": ["src"],
"exclude": ["dist"]
}

View File

@@ -0,0 +1,25 @@
import { defineConfig } from "tsup";
export default defineConfig({
entry: {
"core/index": "src/core/index.ts",
"react/index": "src/react/index.tsx",
"angular/index": "src/angular/index.ts",
},
format: ["esm", "cjs"],
dts: true,
sourcemap: true,
clean: false,
target: "es2022",
minify: false,
treeshake: true,
splitting: false,
external: [
"react",
"react-dom",
"antd",
"@angular/core",
"@angular/common",
"@hublib-web/tach-typography/react",
],
});