9 Commits

451 changed files with 16145 additions and 7193 deletions

13
.gitignore vendored
View File

@@ -1,3 +1,12 @@
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/sdks
!.yarn/versions
node_modules node_modules
*.log **/node_modules
storybook-static **/.turbo
coverage
.DS_Store
**/storybook-static

View File

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

View File

@@ -1,25 +0,0 @@
:root {
--Default-BgLight: #ffffff;
--Default-BgDarken: #f2f2f2;
--Default-StrokeDividers: #e0e0e0;
--Default-White: #ffffff;
--Text-Primary: #1d1d1d;
--Controls-Primary: #1d1d1d;
--Controls-Plashes: #f5f5f5;
--Shadow-Z100: 0 10px 24px rgba(20, 20, 20, 0.14);
--Accent-Primary: #5152ba;
--Opacity-BlackOpacity45: rgba(0, 0, 0, 0.45);
--corner-S: 8px;
--corner-M: 12px;
--Corner-XL: 32px;
}
html,
body,
#storybook-root {
height: 100%;
}
body {
background: #f6f8fb;
}

View File

@@ -1,22 +0,0 @@
import type { Preview } from "@storybook/react";
import "antd/dist/reset.css";
import "../src/react/video-player/tach-video-js.css";
import "./preview.css";
const preview: Preview = {
parameters: {
layout: "padded",
controls: {
expanded: true,
sort: "requiredFirst",
},
options: {
storySort: {
order: ["React", "Angular"],
},
},
},
};
export default preview;

8
.yarnrc.yml Normal file
View File

@@ -0,0 +1,8 @@
enableGlobalCache: false
nodeLinker: node-modules
packageExtensions:
ng-zorro-antd@*:
peerDependencies:
rxjs: ">=7.0.0"

215
README.md
View File

@@ -1,193 +1,60 @@
# @tach/video-player # hublib-web
Single package with three entrypoints: Monorepo for shared UI packages distributed with Yarn workspaces.
- `@tach/video-player/core` External projects install these packages directly from Git tags. Package `dist/` artifacts must be committed.
- `@tach/video-player/react`
- `@tach/video-player/angular`
This folder is prepared to be moved into a dedicated repository as-is. ## Structure
## What Is Included - `packages/tach-typography` - typography tokens and adapters for React/Angular.
- `packages/video-player` - cross-framework video player runtime and adapters.
- `packages/content-suggestions` - content text/title with mention/tag/link parsing for React/Angular.
- Build pipeline to `dist/`: ## Package READMEs
- `npm run build`
- `npm run typecheck`
- Storybook playground for framework entrypoints:
- `npm run storybook`
- `npm run build-storybook`
- `dist`-first flow for git-based installation:
- `dist/` is committed to the repository.
- consumers do not depend on `prepare` at install time.
- Version bump scripts:
- `npm run release:patch`
- `npm run release:minor`
- `npm run release:major`
- Optional framework peers:
- React peers are optional if only `core/angular` is used.
- Angular peers are optional if only `core/react` is used.
## Repo Transfer Checklist - [tach-typography](./packages/tach-typography/README.md)
- [video-player](./packages/video-player/README.md)
- [content-suggestions](./packages/content-suggestions/README.md)
1. Create a new repository (for example `tach/video-player`). ## Development
2. Copy everything from this folder to the new repo root.
3. In the new repo run:
```bash ```bash
npm install corepack enable
npm run build yarn set version 4.12.0
yarn install
yarn build
yarn test
``` ```
4. Commit and push (including `dist/`): ## Add a new package
1. Create a folder inside `packages/<name>`.
2. Add `package.json` with a unique package name.
3. Link internal dependencies with `workspace:*` (or `workspace:^`).
4. Run `yarn install` and then `yarn build`.
## Installation from Git (SSH)
This repository is consumed by tag-based Git dependencies.
- Full install guide: [docs/git-installation.md](./docs/git-installation.md)
- Release workflow: [docs/release-policy.md](./docs/release-policy.md)
Command template:
```bash ```bash
git add . yarn add "@hublib-web/<package>@git+ssh://git@github.com/ORG/REPO.git#workspace=@hublib-web/<package>&tag=<package>-vX.Y.Z"
git commit -m "feat: initial @tach/video-player package"
git push origin main
``` ```
5. Create a release tag: ## Releases
```bash Releases are done with Git tags (no Changesets, no npm publish).
git tag v0.1.0
git push origin v0.1.0
```
## Build Output Short flow:
`npm run build` compiles TypeScript into `dist/` and copies required assets (`css/scss/svg/...`). 1. Bump version in `packages/<package>/package.json`.
`dist/` must be committed before creating a new version tag. 2. Build package and commit updated `dist/` artifacts.
3. Create an annotated package tag: `<package>-vX.Y.Z`.
4. Push commit and tags to origin.
Entrypoints are exported from `dist`: Detailed policy and examples: [docs/release-policy.md](./docs/release-policy.md).
- `@tach/video-player/core`
- `@tach/video-player/react`
- `@tach/video-player/angular`
## Storybook
Local Storybook exposes two pages for development and prop testing:
- `React/VideoPlayer` - React component entrypoint with controls for player props.
- `Angular/VideoPlayerAdapter` - adapter entrypoint with controls for runtime options (`attach/update`).
Run Storybook:
```bash
npm run storybook
```
Build static Storybook:
```bash
npm run build-storybook
```
## Installation From Git (Without npm Registry)
### npm
By tag:
```bash
npm i git+ssh://git@github.com/<org>/video-player.git#v0.1.0
```
By commit:
```bash
npm i git+ssh://git@github.com/<org>/video-player.git#<commit-sha>
```
By semver tag range:
```bash
npm i github:<org>/video-player#semver:^0.1.0
```
### pnpm
```bash
pnpm add git+ssh://git@github.com/<org>/video-player.git#v0.1.0
```
### yarn
```bash
yarn add git+ssh://git@github.com/<org>/video-player.git#v0.1.0
```
## Versioning Workflow
1. Build and bump version:
```bash
npm run release:patch
# or release:minor / release:major
```
2. Commit generated `dist` changes and `package.json` version bump:
```bash
git add .
git commit -m "chore(release): vX.Y.Z"
```
3. Push commit and tags:
```bash
git push origin main --follow-tags
```
4. Consumers update git tag/version in their `package.json`.
## Why Optional Peers
Consumers install only the framework they need:
- React app: install `react` and `react-dom`, use `@tach/video-player/react`.
- Angular app: install `@angular/*`, use `@tach/video-player/angular`.
- Shared utilities only: use `@tach/video-player/core`.
## Entrypoints
### Core
```ts
import {
isHlsSource,
selectPlaybackEngine,
VideoPlayerRuntime,
setVideoPlayerTokenProvider,
} from "@tach/video-player/core";
setVideoPlayerTokenProvider(async () => {
// Provide host-app token retrieval here.
return null;
});
const runtime = new VideoPlayerRuntime();
await runtime.init({
container: document.getElementById("player")!,
source: { src: "https://example.com/video.m3u8" },
});
```
### React
```tsx
import VideoPlayer from "@tach/video-player/react";
```
### Angular
```ts
import { AngularVideoPlayerAdapter } from "@tach/video-player/angular";
const adapter = new AngularVideoPlayerAdapter();
await adapter.attach(containerElement, {
source: { src: "https://example.com/video.m3u8" },
});
```
`AngularVideoPlayerAdapter` is intentionally framework-light: it wraps `VideoPlayerRuntime` (`attach/update/destroy/on`) and does not depend on React code.

View File

@@ -1,11 +0,0 @@
export const formatTime = (seconds) => {
const pad = (num) => String(num).padStart(2, "0");
const hrs = Math.floor(seconds / 3600);
const mins = Math.floor((seconds % 3600) / 60);
const secs = Math.floor(seconds % 60);
if (seconds < 3600) {
return `${pad(mins)}:${pad(secs)}`;
}
return `${pad(hrs)}:${pad(mins)}:${pad(secs)}`;
};
//# sourceMappingURL=format-time.js.map

View File

@@ -1 +0,0 @@
{"version":3,"file":"format-time.js","sourceRoot":"","sources":["../../src/core/format-time.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,UAAU,GAAG,CAAC,OAAe,EAAE,EAAE;IAC7C,MAAM,GAAG,GAAG,CAAC,GAAW,EAAE,EAAE,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;IAC1D,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,GAAG,IAAI,CAAC,CAAC;IACvC,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,OAAO,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC;IAC/C,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,GAAG,EAAE,CAAC,CAAC;IAEtC,IAAI,OAAO,GAAG,IAAI,EAAE,CAAC;QACpB,OAAO,GAAG,GAAG,CAAC,IAAI,CAAC,IAAI,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC;IACpC,CAAC;IAED,OAAO,GAAG,GAAG,CAAC,GAAG,CAAC,IAAI,GAAG,CAAC,IAAI,CAAC,IAAI,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC;AAChD,CAAC,CAAC"}

File diff suppressed because one or more lines are too long

View File

@@ -1,25 +0,0 @@
import React from "react";
import Player from "video.js/dist/types/player";
interface VideoQRScannerPluginProps {
player: Player | null;
enabled?: boolean;
/**
* Интервал между запусками сканирования в миллисекундах.
* По умолчанию 200 мс.
*/
scanInterval?: number;
/**
* Масштаб для сканирования (0 < scanningScale <= 1).
* Значение меньше 1 уменьшает разрешение для ускорения обработки.
* По умолчанию 1.
*/
scanningScale?: number;
/**
* Количество неудачных попыток обнаружения QR-кода до снятия маски.
* По умолчанию 2.a
*/
maxFailedAttempts?: number;
}
declare const VideoQRScannerPlugin: React.FC<VideoQRScannerPluginProps>;
export default VideoQRScannerPlugin;
//# sourceMappingURL=index.d.ts.map

View File

@@ -1 +0,0 @@
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/react/video-player/components/qr-scanner/index.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAmD,MAAM,OAAO,CAAC;AAKxE,OAAO,MAAM,MAAM,4BAA4B,CAAC;AAIhD,UAAU,yBAAyB;IAClC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;;OAIG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;;OAGG;IACH,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED,QAAA,MAAM,oBAAoB,EAAE,KAAK,CAAC,EAAE,CAAC,yBAAyB,CAqR7D,CAAC;AAEF,eAAe,oBAAoB,CAAC"}

View File

@@ -1,209 +0,0 @@
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
import { useCallback, useEffect, useRef, useState } from "react";
import { createPortal } from "react-dom";
import { Popover } from "antd";
import QrScanner from "qr-scanner";
import { CloseOutlined } from "@ant-design/icons";
const VideoQRScannerPlugin = ({ player, enabled = true, scanInterval = 200, scanningScale = 1, maxFailedAttempts = 2, }) => {
const canvasRef = useRef(null);
const [detectedQr, setDetectedQr] = useState(null);
const animationFrameRef = useRef(null);
const ignoredQRCodes = useRef(new Set());
const [playerEl, setPlayerEl] = useState(null);
const lastScanTimeRef = useRef(0);
const isScanningRef = useRef(false);
// Счётчик неудачных попыток обнаружения QR, если код уже отображается
const failedAttemptsRef = useRef(0);
// Для получения актуального значения detectedQr внутри асинхронного колбэка
const detectedQrRef = useRef(null);
useEffect(() => {
detectedQrRef.current = detectedQr;
}, [detectedQr]);
// Изначально устанавливаем контейнер для портала как элемент плеера
useEffect(() => {
if (player) {
setPlayerEl(player.el());
}
}, [player]);
// Обновление контейнера при переходе в полноэкранный режим
useEffect(() => {
const handleFullscreenChange = () => {
if (document.fullscreenElement) {
setPlayerEl(document.fullscreenElement);
}
else if (player) {
setPlayerEl(player.el());
}
};
document.addEventListener("fullscreenchange", handleFullscreenChange);
return () => {
document.removeEventListener("fullscreenchange", handleFullscreenChange);
};
}, [player]);
const scanFrame = useCallback(async () => {
if (!player || player.paused() || !canvasRef.current || !enabled)
return;
const now = performance.now();
if (now - lastScanTimeRef.current < scanInterval) {
animationFrameRef.current = requestAnimationFrame(scanFrame);
return;
}
lastScanTimeRef.current = now;
// Предотвращаем параллельное выполнение сканирования
if (isScanningRef.current) {
animationFrameRef.current = requestAnimationFrame(scanFrame);
return;
}
isScanningRef.current = true;
const videoEl = player.tech(true).el();
const canvas = canvasRef.current;
const ctx = canvas.getContext("2d");
if (!ctx) {
isScanningRef.current = false;
return;
}
// Получаем оригинальные размеры видео
const originalVideoWidth = videoEl.videoWidth;
const originalVideoHeight = videoEl.videoHeight;
// Устанавливаем canvas с пониженным разрешением для сканирования
canvas.width = originalVideoWidth * scanningScale;
canvas.height = originalVideoHeight * scanningScale;
// Рисуем видео в canvas с пониженным разрешением
ctx.drawImage(videoEl, 0, 0, canvas.width, canvas.height);
try {
const result = await QrScanner.scanImage(canvas, {
returnDetailedScanResult: true,
});
if (result &&
result.data &&
result.cornerPoints?.length === 4 &&
!ignoredQRCodes.current.has(result.data)) {
// Сброс неудачных попыток при успешном обнаружении
failedAttemptsRef.current = 0;
// Преобразуем координаты из масштабированного canvas в координаты оригинального видео
const points = result.cornerPoints.map(p => ({
x: p.x / scanningScale,
y: p.y / scanningScale,
}));
const minX = Math.min(...points.map(p => p.x));
const minY = Math.min(...points.map(p => p.y));
const maxX = Math.max(...points.map(p => p.x));
const maxY = Math.max(...points.map(p => p.y));
// Получаем размеры отображаемого видео
const rect = videoEl.getBoundingClientRect();
const displayScale = Math.min(rect.width / originalVideoWidth, rect.height / originalVideoHeight);
const offsetX = (rect.width - originalVideoWidth * displayScale) / 2;
const offsetY = (rect.height - originalVideoHeight * displayScale) / 2;
const qrWidth = (maxX - minX) * displayScale;
const qrHeight = (maxY - minY) * displayScale;
const padding = 8;
const x = minX * displayScale + offsetX - padding;
const y = minY * displayScale + offsetY - padding;
setDetectedQr({
data: result.data,
position: new DOMRect(x, y, qrWidth + padding * 2, qrHeight + padding * 2),
});
}
else {
// Если код не найден, и он уже отображался, даем maxFailedAttempts попыток
if (detectedQrRef.current) {
failedAttemptsRef.current += 1;
if (failedAttemptsRef.current >= maxFailedAttempts) {
setDetectedQr(null);
failedAttemptsRef.current = 0;
}
}
}
}
catch (error) {
// В случае ошибки аналогичная логика: если код уже отображался — даем maxFailedAttempts попыток
if (detectedQrRef.current) {
failedAttemptsRef.current += 1;
if (failedAttemptsRef.current >= maxFailedAttempts) {
setDetectedQr(null);
failedAttemptsRef.current = 0;
}
}
}
finally {
isScanningRef.current = false;
}
animationFrameRef.current = requestAnimationFrame(scanFrame);
}, [player, enabled, scanInterval, scanningScale, maxFailedAttempts]);
useEffect(() => {
if (!player || !enabled)
return;
const startScanning = () => {
if (animationFrameRef.current)
cancelAnimationFrame(animationFrameRef.current);
animationFrameRef.current = requestAnimationFrame(scanFrame);
};
const stopScanning = () => {
if (animationFrameRef.current) {
cancelAnimationFrame(animationFrameRef.current);
animationFrameRef.current = null;
}
setDetectedQr(null);
};
player.on("play", startScanning);
player.on("pause", stopScanning);
player.on("ended", stopScanning);
window.addEventListener("resize", startScanning);
return () => {
stopScanning();
player.off("play", startScanning);
player.off("pause", stopScanning);
player.off("ended", stopScanning);
window.removeEventListener("resize", startScanning);
};
}, [player, enabled, scanFrame]);
const ignoreQRCode = () => {
if (detectedQr) {
ignoredQRCodes.current.add(detectedQr.data);
setDetectedQr(null);
}
};
return (_jsxs(_Fragment, { children: [_jsx("canvas", { ref: canvasRef, style: { display: "none" } }), detectedQr &&
playerEl &&
createPortal(_jsx(Popover, { getPopupContainer: () => playerEl, content: _jsx("a", { href: detectedQr.data, target: "_blank", rel: "noreferrer", referrerPolicy: "no-referrer", style: { color: "#1677ff" }, children: detectedQr.data }), placement: "top", children: _jsxs("div", { style: {
position: "absolute",
left: detectedQr.position.x,
top: detectedQr.position.y,
width: detectedQr.position.width,
height: detectedQr.position.height,
pointerEvents: "auto",
cursor: "pointer",
zIndex: 10,
}, children: [["top-left", "top-right", "bottom-left", "bottom-right"].map(corner => (_jsx("div", { style: {
position: "absolute",
width: 20,
height: 20,
border: "3px solid var(--Accent-Primary)",
borderRadius: 4,
...(corner.includes("top") ? { top: 0 } : { bottom: 0 }),
...(corner.includes("left") ? { left: 0 } : { right: 0 }),
borderTop: corner.includes("bottom")
? "none"
: "3px solid var(--Accent-Primary)",
borderBottom: corner.includes("top")
? "none"
: "3px solid var(--Accent-Primary)",
borderLeft: corner.includes("right")
? "none"
: "3px solid var(--Accent-Primary)",
borderRight: corner.includes("left")
? "none"
: "3px solid var(--Accent-Primary)",
} }, corner))), _jsx(CloseOutlined, { style: {
color: "#ff4d4f",
position: "absolute",
top: "-8px",
right: "-8px",
background: "white",
borderRadius: "50%",
padding: "2px",
cursor: "pointer",
}, onClick: ignoreQRCode })] }) }), playerEl)] }));
};
export default VideoQRScannerPlugin;
//# sourceMappingURL=index.js.map

File diff suppressed because one or more lines are too long

View File

@@ -1 +0,0 @@
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../../../src/react/video-player/components/video-js/plugins/settings/index.ts"],"names":[],"mappings":"AACA,OAAO,SAAS,MAAM,+BAA+B,CAAC;AAEtD,OAAO,MAAM,MAAM,4BAA4B,CAAC;AAYhD,OAAO,gBAAgB,CAAC;AAExB,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAG5C,UAAU,qBAAqB;CAE9B;AAGD,UAAU,oBAAqB,SAAQ,aAAa;IACnD,UAAU,EAAE,SAAS,CAAC;CACtB;AAED,QAAA,MAAM,UAAU,EAAmC,OAAO,MAAM,CAAC;AAKjE,cAAM,cAAe,SAAQ,UAAU;IACtC,OAAO,CAAC,OAAO,CAAwB;IACvC,OAAO,CAAC,QAAQ,CAA6B;IAC7C,OAAO,CAAC,cAAc,CAAuB;IAC7C,OAAO,CAAC,cAAc,CAAa;IAGnC,OAAO,CAAC,UAAU,CAAuB;IACzC,OAAO,CAAC,kBAAkB,CAA6B;IACvD,OAAO,CAAC,iBAAiB,CAA6B;IACtD,OAAO,CAAC,iBAAiB,CAA6B;IACtD,OAAO,CAAC,gBAAgB,CAA6B;gBAEzC,MAAM,EAAE,oBAAoB,EAAE,OAAO,EAAE,qBAAqB;IAMxE;;OAEG;IACH,OAAO,CAAC,UAAU;IAKlB;;OAEG;IACH,OAAO,CAAC,gBAAgB;IAKxB;;;OAGG;IACH,OAAO,CAAC,oBAAoB;IA8D5B;;;;;OAKG;IACH,OAAO,CAAC,WAAW;IAInB;;;;;;OAMG;YACW,OAAO;CA8CrB;AAID,eAAe,cAAc,CAAC"}

View File

@@ -1 +0,0 @@
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../../../../../src/react/video-player/components/video-js/plugins/settings/selectors/text-track-selector/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAClD,OAAO,EAAE,yBAAyB,EAAE,MAAM,gBAAgB,CAAC;AAS3D,UAAU,kBAAmB,SAAQ,yBAAyB;CAAG;AAEjE,QAAA,MAAM,UAAU,GACf,QAAQ,aAAa,EACrB,UAAS,kBAA6B;;;;;;;;;;;;;;;CAgDtC,CAAC;AAEF,eAAe,UAAU,CAAC"}

View File

@@ -1 +0,0 @@
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../../../../../../src/react/video-player/components/video-js/plugins/settings/selectors/text-track-selector/index.ts"],"names":[],"mappings":"AAGA,MAAM,QAAQ,GAAG;IAChB,KAAK,EAAE,UAAU;IACjB,QAAQ,EAAE,KAAK;IACf,UAAU,EAAE,KAAK;IACjB,SAAS,EAAE,eAAe;CAC1B,CAAC;AAIF,MAAM,UAAU,GAAG,CAClB,MAAqB,EACrB,UAA8B,QAAQ,EACrC,EAAE;IACH,MAAM,SAAS,GAAG,GAAG,EAAE;QACtB,MAAM,UAAU,GAAG,MAAM,CAAC,UAAU,EAAE,CAAC,OAAO,CAAC;QAC/C,MAAM,UAAU,GAAG,EAAE,CAAC;QAEtB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,UAAU,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,CAAC;YAC7C,IAAI,UAAU,CAAC,CAAC,CAAC,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;gBACrC,SAAS;YACV,CAAC;YAED,MAAM,SAAS,GAAG;gBACjB,KAAK,EAAE,UAAU,CAAC,CAAC,CAAC,CAAC,KAAK;gBAC1B,KAAK,EAAE,UAAU,CAAC,CAAC,CAAC,CAAC,KAAK;gBAC1B,QAAQ,EAAE,UAAU,CAAC,CAAC,CAAC,CAAC,IAAI,KAAK,SAAS;gBAC1C,OAAO,EAAE,GAAG,EAAE,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC;aACtC,CAAC;YAEF,UAAU,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;QAC5B,CAAC;QAED,OAAO,UAAU,CAAC;IACnB,CAAC,CAAC;IAEF,MAAM,QAAQ,GAAG,CAAC,KAAgB,EAAE,EAAE;QACrC,MAAM,UAAU,GAAG,MAAM,CAAC,UAAU,EAAE,CAAC,OAAO,CAAC;QAE/C,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,UAAU,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,CAAC;YAC7C,IAAI,UAAU,CAAC,CAAC,CAAC,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;gBACrC,SAAS;YACV,CAAC;YAED,IAAI,UAAU,CAAC,CAAC,CAAC,CAAC,KAAK,KAAK,KAAK,CAAC,KAAK;gBAAE,UAAU,CAAC,CAAC,CAAC,CAAC,IAAI,GAAG,SAAS,CAAC;;gBACnE,UAAU,CAAC,CAAC,CAAC,CAAC,IAAI,GAAG,UAAU,CAAC;QACtC,CAAC;IACF,CAAC,CAAC;IAEF,MAAM,QAAQ,GAAG,GAAG,EAAE;QACrB,OAAO;YACN,GAAG,OAAO;YACV,OAAO,EAAE,SAAS,EAAE,CAAC,MAAM,GAAG,CAAC;SAC/B,CAAC;IACH,CAAC,CAAC;IAEF,OAAO;QACN,SAAS;QACT,QAAQ;KACR,CAAC;AACH,CAAC,CAAC;AAEF,eAAe,UAAU,CAAC"}

View File

@@ -1,51 +0,0 @@
import Player from "video.js/dist/types/player";
import { SkipButtonsOptions } from "./plugins/skip-buttons";
export interface IVideoJSSource {
src: string;
type: string;
}
export type PreloadType = "auto" | "metadata" | "none" | "visibility";
export interface IVideoJSOptions {
autoplay: boolean;
controls: boolean;
responsive: boolean;
aspectRatio?: string;
preload: PreloadType;
fluid: boolean;
muted: boolean;
sources: IVideoJSSource[];
poster?: string;
preferHQ?: boolean;
/** Включить детальное логирование */
debug?: boolean;
}
export interface IVideoJSProps {
options: IVideoJSOptions;
onReady?: (player: VideoJsPlayer) => void;
className?: string;
classNames?: string[];
initialTime?: number;
full?: boolean;
withRewind?: boolean;
}
export interface VideoJsLiveTracker {
isLive_: boolean;
atLiveEdge?: () => boolean;
startTracking: () => void;
trigger: (event: string) => void;
}
export type VideoJsPlayer = Player & {
liveTracker?: VideoJsLiveTracker;
settingsMenu?: () => void;
mobileUi?: () => void;
bigPlayPauseButton?: () => void;
skipButtons?: (options: SkipButtonsOptions) => void;
subscribeToSegmentChange: (callback: (segment: any) => void) => void;
subscribeToDuration: (callback: (duration: number) => void) => void;
subscribeToPlayStart: (callback: () => void) => void;
subscribeToPlayStarted: (callback: () => void) => void;
subscribeToManifestLoaded: (callback: () => void) => void;
mediaduration: () => number | undefined;
textTracks: () => TextTrack[];
};
//# sourceMappingURL=types.d.ts.map

View File

@@ -1 +0,0 @@
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../../../src/react/video-player/components/video-js/types.ts"],"names":[],"mappings":"AAAA,OAAO,MAAM,MAAM,4BAA4B,CAAC;AAEhD,OAAO,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAC;AAE5D,MAAM,WAAW,cAAc;IAC9B,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;CACb;AAED,MAAM,MAAM,WAAW,GAAG,MAAM,GAAG,UAAU,GAAG,MAAM,GAAG,YAAY,CAAC;AAEtE,MAAM,WAAW,eAAe;IAC/B,QAAQ,EAAE,OAAO,CAAC;IAClB,QAAQ,EAAE,OAAO,CAAC;IAClB,UAAU,EAAE,OAAO,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE,WAAW,CAAC;IACrB,KAAK,EAAE,OAAO,CAAC;IACf,KAAK,EAAE,OAAO,CAAC;IACf,OAAO,EAAE,cAAc,EAAE,CAAC;IAC1B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,qCAAqC;IACrC,KAAK,CAAC,EAAE,OAAO,CAAC;CAChB;AAED,MAAM,WAAW,aAAa;IAC7B,OAAO,EAAE,eAAe,CAAC;IACzB,OAAO,CAAC,EAAE,CAAC,MAAM,EAAE,aAAa,KAAK,IAAI,CAAC;IAC1C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;IACtB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,UAAU,CAAC,EAAE,OAAO,CAAC;CACrB;AAED,MAAM,WAAW,kBAAkB;IAClC,OAAO,EAAE,OAAO,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,OAAO,CAAC;IAC3B,aAAa,EAAE,MAAM,IAAI,CAAC;IAC1B,OAAO,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CACjC;AAGD,MAAM,MAAM,aAAa,GAAG,MAAM,GAAG;IACpC,WAAW,CAAC,EAAE,kBAAkB,CAAC;IACjC,YAAY,CAAC,EAAE,MAAM,IAAI,CAAC;IAC1B,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;IACtB,kBAAkB,CAAC,EAAE,MAAM,IAAI,CAAC;IAChC,WAAW,CAAC,EAAE,CAAC,OAAO,EAAE,kBAAkB,KAAK,IAAI,CAAC;IACpD,wBAAwB,EAAE,CAAC,QAAQ,EAAE,CAAC,OAAO,EAAE,GAAG,KAAK,IAAI,KAAK,IAAI,CAAC;IACrE,mBAAmB,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,KAAK,IAAI,CAAC;IACpE,oBAAoB,EAAE,CAAC,QAAQ,EAAE,MAAM,IAAI,KAAK,IAAI,CAAC;IACrD,sBAAsB,EAAE,CAAC,QAAQ,EAAE,MAAM,IAAI,KAAK,IAAI,CAAC;IACvD,yBAAyB,EAAE,CAAC,QAAQ,EAAE,MAAM,IAAI,KAAK,IAAI,CAAC;IAC1D,aAAa,EAAE,MAAM,MAAM,GAAG,SAAS,CAAC;IACxC,UAAU,EAAE,MAAM,SAAS,EAAE,CAAC;CAC9B,CAAC"}

View File

@@ -1,11 +0,0 @@
export const formatTime = (seconds) => {
const pad = (num) => String(num).padStart(2, "0");
const hrs = Math.floor(seconds / 3600);
const mins = Math.floor((seconds % 3600) / 60);
const secs = Math.floor(seconds % 60);
if (seconds < 3600) {
return `${pad(mins)}:${pad(secs)}`;
}
return `${pad(hrs)}:${pad(mins)}:${pad(secs)}`;
};
//# sourceMappingURL=utils.js.map

View File

@@ -1 +0,0 @@
{"version":3,"file":"utils.js","sourceRoot":"","sources":["../../../../../src/react/video-player/components/video-js/utils.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,UAAU,GAAG,CAAC,OAAe,EAAE,EAAE;IAC7C,MAAM,GAAG,GAAG,CAAC,GAAW,EAAE,EAAE,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;IAC1D,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,GAAG,IAAI,CAAC,CAAC;IACvC,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,OAAO,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC;IAC/C,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,GAAG,EAAE,CAAC,CAAC;IACtC,IAAI,OAAO,GAAG,IAAI,EAAE,CAAC;QACpB,OAAO,GAAG,GAAG,CAAC,IAAI,CAAC,IAAI,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC;IACpC,CAAC;IACD,OAAO,GAAG,GAAG,CAAC,GAAG,CAAC,IAAI,GAAG,CAAC,IAAI,CAAC,IAAI,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC;AAChD,CAAC,CAAC"}

View File

@@ -1 +0,0 @@
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../../src/react/video-player/components/with-errors/index.tsx"],"names":[],"mappings":";AAAA,OAAO,KAAgC,MAAM,OAAO,CAAC;AAMrD,OAAO,UAAU,MAAM,eAAe,CAAC;AACvC,OAAO,MAAM,MAAM,2BAA2B,CAAC;AAO/C,MAAM,UAAU,GAAG,CAAC,EACnB,UAAU,GAAG,KAAK,EAClB,QAAQ,EACR,GAAG,KAAK,EACqB,EAAE,EAAE;IAEjC,OAAO,CACN,eACC,SAAS,EAAE,MAAM,CAAC,SAAS,EAC3B,KAAK,EAAE,EAAE,WAAW,EAAE,KAAK,CAAC,OAAO,CAAC,WAAW,EAAE,aAEhD,UAAU,IAAI,KAAC,UAAU,IAAC,SAAS,EAAE,MAAM,CAAC,MAAM,GAAI,EACtD,KAAK,CAAC,YAAY,CAAC,QAAQ,EAAE,EAAE,GAAG,KAAK,EAAE,CAAC,IACtC,CACN,CAAC;AACH,CAAC,CAAC;AAEF,eAAe,UAAU,CAAC"}

View File

@@ -1 +0,0 @@
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/react/video-player/components/with-mouse-events/index.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,OAAO,CAAC;AAIjC,OAAO,MAAM,MAAM,4BAA4B,CAAC;AAEhD,OAAO,EAAE,aAAa,EAAiB,MAAM,mBAAmB,CAAC;AAGjE,MAAM,WAAW,qBAAsB,SAAQ,aAAa;IAC3D,OAAO,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAC;IAC1C,YAAY,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAC;IAC/C,YAAY,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAC;IAC/C,QAAQ,EAAE,GAAG,CAAC,OAAO,CAAC;CACtB;AAED,eAAO,MAAM,eAAe,GAAI,sEAO7B,qBAAqB,4CAkEvB,CAAC"}

View File

@@ -1 +0,0 @@
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/react/video-player/components/with-observation/index.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,OAAO,CAAC;AAKjC,OAAO,MAAM,MAAM,4BAA4B,CAAC;AAGhD,OAAO,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AAG7D,MAAM,WAAW,qBAAsB,SAAQ,qBAAqB;IACnE,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAC;IACzC,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAC;IACzC,QAAQ,EAAE,GAAG,CAAC,OAAO,CAAC;CACtB;AAED,eAAO,MAAM,eAAe,GAAI,iDAM7B,qBAAqB,4CAsDvB,CAAC"}

43
docs/git-installation.md Normal file
View File

@@ -0,0 +1,43 @@
# Git installation (SSH + tags)
This repository is consumed directly from Git tags, not from npm.
## Requirements
- You have SSH access to the repository (`git@github.com:ORG/REPO.git`).
- Release tags already exist in remote.
- `dist/` is committed in the tagged commit.
## Tag naming
Use package-specific tags:
- `tach-typography-vX.Y.Z`
- `video-player-vX.Y.Z`
- `content-suggestions-vX.Y.Z`
## Yarn template
Always wrap the dependency string in quotes because of `&tag=`.
```bash
yarn add "@hublib-web/<package>@git+ssh://git@github.com/ORG/REPO.git#workspace=@hublib-web/<package>&tag=<package>-vX.Y.Z"
```
## Package examples
```bash
yarn add "@hublib-web/tach-typography@git+ssh://git@github.com/ORG/REPO.git#workspace=@hublib-web/tach-typography&tag=tach-typography-v0.1.0"
yarn add "@hublib-web/video-player@git+ssh://git@github.com/ORG/REPO.git#workspace=@hublib-web/video-player&tag=video-player-v0.1.0"
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"
```
## Update package version in a consumer app
```bash
yarn up "@hublib-web/video-player@git+ssh://git@github.com/ORG/REPO.git#workspace=@hublib-web/video-player&tag=video-player-v0.2.0"
```
## Related docs
- Release policy: [release-policy.md](./release-policy.md)

54
docs/release-policy.md Normal file
View File

@@ -0,0 +1,54 @@
# Release policy
This monorepo is released through Git tags. We do not use Changesets or npm publish.
## Principles
- Every release points to a commit where the package `dist/` is already built and committed.
- Tags are package-specific, so each package can be released independently.
- Consumers install by tag using Git SSH URLs.
## Tag format
- `tach-typography-vX.Y.Z`
- `video-player-vX.Y.Z`
- `content-suggestions-vX.Y.Z`
`X.Y.Z` must match the version in the package's `package.json`.
## Release checklist (single package)
1. Update package version in `packages/<package>/package.json`.
2. Build package:
```bash
yarn workspace @hublib-web/<package> build
```
3. Verify that `dist/` and `package.json` contain the release changes.
4. Commit release files:
```bash
git add packages/<package>/package.json packages/<package>/dist
git commit -m "release(<package>): vX.Y.Z"
```
5. Create annotated tag:
```bash
git tag -a <package>-vX.Y.Z -m "@hublib-web/<package> vX.Y.Z"
```
6. Push commit and tags:
```bash
git push origin main --follow-tags
```
## Multi-package release
If one commit releases several packages, create one tag per package on the same commit.
## Consumer installation
Use the package-specific tag in dependency spec. See [git-installation.md](./git-installation.md).

21
eslint.config.mjs Normal file
View File

@@ -0,0 +1,21 @@
import js from "@eslint/js";
import tseslint from "typescript-eslint";
export default [
js.configs.recommended,
...tseslint.configs.recommended,
{
files: ["**/*.ts", "**/*.tsx"],
languageOptions: {
ecmaVersion: "latest",
sourceType: "module",
},
rules: {
"no-unused-vars": "off",
"@typescript-eslint/no-unused-vars": ["warn", { "argsIgnorePattern": "^_" }],
},
},
{
ignores: ["**/dist/**", "**/node_modules/**"],
},
];

5994
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,88 +1,27 @@
{ {
"name": "@tach/video-player", "name": "@hublib-web/root",
"version": "0.0.1", "private": true,
"type": "module", "packageManager": "yarn@4.12.0",
"main": "./dist/react/index.js", "workspaces": [
"types": "./dist/react/index.d.ts", "packages/*"
"sideEffects": true,
"files": [
"dist",
"README.md"
], ],
"scripts": { "scripts": {
"build": "node ./scripts/build.mjs", "build": "yarn workspaces foreach -A -p --topological-dev run build",
"typecheck": "tsc -p ./tsconfig.build.json --noEmit", "typecheck": "yarn workspaces foreach -A -p --topological-dev run typecheck",
"storybook": "storybook dev -p 6006", "test": "yarn workspaces foreach -A -p --topological-dev run test",
"build-storybook": "storybook build", "clean": "yarn workspaces foreach -A -p --topological-dev run clean",
"prepack": "npm run build", "lint": "yarn workspaces foreach -A -p --topological-dev run lint"
"release:patch": "npm run build && npm version patch",
"release:minor": "npm run build && npm version minor",
"release:major": "npm run build && npm version major"
},
"exports": {
"./package.json": "./package.json",
"./core": {
"types": "./dist/core/index.d.ts",
"import": "./dist/core/index.js",
"default": "./dist/core/index.js"
},
"./react": {
"types": "./dist/react/index.d.ts",
"import": "./dist/react/index.js",
"default": "./dist/react/index.js"
},
"./angular": {
"types": "./dist/angular/index.d.ts",
"import": "./dist/angular/index.js",
"default": "./dist/angular/index.js"
}
},
"dependencies": {
"@ant-design/icons": "5.3.7",
"antd": "5.14.2",
"classnames": "2.5.1",
"hls.js": "1.6.14",
"lodash": "4.17.21",
"qr-scanner": "1.4.2",
"react-device-detect": "2.2.3",
"video.js": "8.23.4"
},
"peerDependencies": {
"@angular/common": "^17.0.0 || ^18.0.0 || ^19.0.0 || ^20.0.0",
"@angular/core": "^17.0.0 || ^18.0.0 || ^19.0.0 || ^20.0.0",
"@angular/forms": "^17.0.0 || ^18.0.0 || ^19.0.0 || ^20.0.0",
"react": "^18.0.0 || ^19.0.0",
"react-dom": "^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@angular/common": {
"optional": true
},
"@angular/core": {
"optional": true
},
"@angular/forms": {
"optional": true
},
"react": {
"optional": true
},
"react-dom": {
"optional": true
}
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.37.0",
"@storybook/addon-essentials": "8.6.14", "@storybook/addon-essentials": "8.6.14",
"@storybook/react": "8.6.14",
"@storybook/react-vite": "8.6.14", "@storybook/react-vite": "8.6.14",
"@types/node": "22.12.0", "@types/node": "^24.6.1",
"@types/react": "19.0.2", "eslint": "^9.37.0",
"@types/react-dom": "19.0.2", "prettier": "^3.6.2",
"react": "19.0.0",
"react-dom": "19.0.0",
"sass": "1.83.4",
"storybook": "8.6.14", "storybook": "8.6.14",
"typescript": "5.9.2", "typescript": "^5.9.3",
"vite": "5.4.14" "typescript-eslint": "^8.57.0",
"vitest": "^3.2.4"
} }
} }

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,126 @@
# @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
Tokenization helper:
```ts
import { createAngularContentTokens } from "@hublib-web/content-suggestions/angular";
const tokens = createAngularContentTokens(text);
```
Ready-to-use UI renderer (React-like props API):
```ts
import { AngularContentTextWithSuggestionsRenderer } from "@hublib-web/content-suggestions/angular";
const renderer = new AngularContentTextWithSuggestionsRenderer();
renderer.attach(containerElement, {
text,
weight: "normal",
ellipsis: { count: 180, expandable: true },
blur: false,
onView: () => {
// analytics
},
});
```
Title variant with defaults (`weight: "bold"`, `ellipsis: { rows: 2 }`):
```ts
import { AngularContentTitleWithSuggestionsRenderer } from "@hublib-web/content-suggestions/angular";
const titleRenderer = new AngularContentTitleWithSuggestionsRenderer();
titleRenderer.attach(titleContainer, { text: title });
```
## 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,495 @@
'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 LINK_COLOR = "#1677ff";
var READ_MORE_TEXT = "\u0427\u0438\u0442\u0430\u0442\u044C \u043F\u043E\u043B\u043D\u043E\u0441\u0442\u044C\u044E";
var toKebabCase = (value) => value.replace(/[A-Z]/g, (char) => `-${char.toLowerCase()}`);
var applyStyleObject = (element, styles) => {
if (!styles) {
return;
}
for (const [key, rawValue] of Object.entries(styles)) {
if (rawValue === null || rawValue === void 0) {
continue;
}
const styleValue = typeof rawValue === "number" ? `${rawValue}px` : String(rawValue);
if (key.startsWith("--")) {
element.style.setProperty(key, styleValue);
continue;
}
const cssKey = key.includes("-") ? key : toKebabCase(key);
element.style.setProperty(cssKey, styleValue);
}
};
var toNode = (value) => {
if (value === null || value === void 0) {
return null;
}
if (value instanceof Node) {
return value;
}
return document.createTextNode(String(value));
};
var resolveEllipsisSymbol = (symbol, expanded) => {
if (typeof symbol === "function") {
return symbol(expanded);
}
return symbol ?? READ_MORE_TEXT;
};
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
};
}
};
var AngularContentTextRenderer = class {
host = null;
props = {};
expanded = false;
viewed = false;
hostInsideView = false;
observer = null;
attach(host, props = {}) {
this.destroy();
this.host = host;
this.props = { ...props };
this.expanded = false;
this.viewed = false;
this.hostInsideView = false;
this.initObserver();
this.render();
return this.getState();
}
update(nextProps) {
this.props = {
...this.props,
...nextProps
};
this.render();
return this.getState();
}
destroy() {
if (this.observer && this.host) {
this.observer.unobserve(this.host);
this.observer.disconnect();
}
this.observer = null;
if (this.host) {
this.host.innerHTML = "";
}
this.host = null;
this.props = {};
this.expanded = false;
this.viewed = false;
this.hostInsideView = false;
}
getState() {
return {
props: { ...this.props },
snapshot: this.createSnapshot(),
expanded: this.getMergedExpanded(this.resolveEllipsisConfig(), this.getText())
};
}
getText() {
return this.props.text ?? "";
}
createSnapshot() {
const text = this.getText();
const entities = findAllEntities(text);
const tokens = createAngularContentTokens(text);
return {
text,
entities,
tokens
};
}
resolveEllipsisConfig() {
const ellipsis = this.props.ellipsis;
if (!ellipsis || typeof ellipsis !== "object") {
return null;
}
return ellipsis;
}
getMergedExpanded(ellipsisConfig, text) {
const controlledExpanded = ellipsisConfig && typeof ellipsisConfig.expanded === "boolean" ? ellipsisConfig.expanded : void 0;
if (controlledExpanded !== void 0) {
return controlledExpanded;
}
if (ellipsisConfig && "count" in ellipsisConfig) {
const count = ellipsisConfig.count ?? 0;
if (count <= 0 || text.length <= count) {
return true;
}
}
return this.expanded;
}
createDefaultMentionNode(entity) {
const span = document.createElement("span");
span.style.color = LINK_COLOR;
span.style.fontWeight = this.props.weight === "bold" ? "700" : "400";
span.textContent = entity.displayText;
return span;
}
createDefaultTagNode(entity) {
const span = document.createElement("span");
span.style.color = LINK_COLOR;
span.style.fontWeight = this.props.weight === "bold" ? "700" : "400";
span.textContent = entity.text;
return span;
}
createDefaultLinkNode(entity) {
const anchor = document.createElement("a");
anchor.href = entity.url;
anchor.target = "_blank";
anchor.referrerPolicy = "no-referrer";
anchor.style.color = LINK_COLOR;
anchor.style.fontWeight = this.props.weight === "bold" ? "700" : "400";
anchor.textContent = entity.text;
return anchor;
}
renderEntity(entity, index) {
if (entity.type === "mention") {
const customNode2 = this.props.renderMention?.(entity, index);
return toNode(customNode2) ?? this.createDefaultMentionNode(entity);
}
if (entity.type === "tag") {
const customNode2 = this.props.renderTag?.(entity, index);
return toNode(customNode2) ?? this.createDefaultTagNode(entity);
}
const customNode = this.props.renderLink?.(entity, index);
return toNode(customNode) ?? this.createDefaultLinkNode(entity);
}
buildTextNodes(text, entities, upto = null) {
let lastIndex = 0;
const nodes = [];
for (const [index, 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(document.createTextNode(text.slice(lastIndex, textEnd)));
}
if (upto === null || entity.end <= upto) {
const entityNode = this.renderEntity(entity, index);
if (entityNode) {
nodes.push(entityNode);
}
}
lastIndex = entity.end;
}
if (upto === null) {
if (lastIndex < text.length) {
nodes.push(document.createTextNode(text.slice(lastIndex)));
}
} else if (lastIndex < upto) {
nodes.push(document.createTextNode(text.slice(lastIndex, upto)));
}
return nodes;
}
applyBaseParagraphStyle(element) {
element.style.whiteSpace = "pre-wrap";
element.style.fontWeight = this.props.weight === "bold" ? "700" : "400";
element.style.setProperty("-webkit-touch-callout", "default");
element.style.setProperty("-webkit-user-select", "text");
element.style.setProperty("-khtml-user-select", "text");
element.style.setProperty("-moz-user-select", "text");
element.style.setProperty("-ms-user-select", "text");
element.style.setProperty("user-select", "text");
if (this.props.blur) {
element.style.filter = "blur(3px)";
element.style.setProperty("-webkit-user-select", "none");
element.style.setProperty("-khtml-user-select", "none");
element.style.setProperty("-moz-user-select", "none");
element.style.setProperty("-ms-user-select", "none");
element.style.setProperty("user-select", "none");
element.style.pointerEvents = "none";
}
applyStyleObject(element, this.props.style);
}
buildReadMoreButton(symbol) {
const button = document.createElement("button");
button.type = "button";
button.textContent = resolveEllipsisSymbol(symbol, this.expanded);
button.style.border = "0";
button.style.background = "none";
button.style.padding = "0";
button.style.marginLeft = "6px";
button.style.cursor = "pointer";
button.style.color = LINK_COLOR;
button.style.fontWeight = "700";
button.onclick = (event) => {
event.preventDefault();
event.stopPropagation();
this.handleExpand();
};
return button;
}
handleExpand() {
const ellipsisConfig = this.resolveEllipsisConfig();
if (!ellipsisConfig) {
return;
}
this.expanded = true;
ellipsisConfig.onExpand?.(true);
this.render();
}
computeEntitySafeCutoff(count, entities) {
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;
}
}
}
return cutoff;
}
hasVerticalOverflow(element) {
return element.scrollHeight - element.clientHeight > 1;
}
render() {
if (!this.host) {
return;
}
const text = this.getText();
const entities = findAllEntities(text);
const ellipsisConfig = this.resolveEllipsisConfig();
const mergedExpanded = this.getMergedExpanded(ellipsisConfig, text);
const wrapper = document.createElement("div");
const paragraph = document.createElement("div");
if (this.props.className) {
paragraph.className = this.props.className;
}
this.applyBaseParagraphStyle(paragraph);
if (ellipsisConfig && "count" in ellipsisConfig) {
const count = ellipsisConfig.count ?? 0;
const shouldTruncate = !mergedExpanded && count > 0 && text.length > count;
if (shouldTruncate) {
const cutoff = this.computeEntitySafeCutoff(count, entities);
const nodes = this.buildTextNodes(text, entities, cutoff);
paragraph.append(...nodes);
paragraph.append(document.createTextNode("\u2026"));
if (ellipsisConfig.expandable) {
paragraph.append(this.buildReadMoreButton(ellipsisConfig.symbol));
}
} else {
paragraph.append(...this.buildTextNodes(text, entities));
}
} else {
paragraph.append(...this.buildTextNodes(text, entities));
if (ellipsisConfig && "rows" in ellipsisConfig && !mergedExpanded) {
paragraph.style.display = "-webkit-box";
paragraph.style.setProperty("-webkit-box-orient", "vertical");
paragraph.style.setProperty("-webkit-line-clamp", String(ellipsisConfig.rows));
paragraph.style.overflow = "hidden";
wrapper.append(paragraph);
this.host.replaceChildren(wrapper);
if (ellipsisConfig.expandable && this.hasVerticalOverflow(paragraph)) {
wrapper.append(this.buildReadMoreButton(ellipsisConfig.symbol));
}
this.emitOnViewIfNeeded(mergedExpanded);
return;
}
}
wrapper.append(paragraph);
this.host.replaceChildren(wrapper);
this.emitOnViewIfNeeded(mergedExpanded);
}
initObserver() {
if (!this.host) {
return;
}
if (typeof IntersectionObserver === "undefined") {
this.hostInsideView = true;
return;
}
this.observer = new IntersectionObserver(
(entries) => {
const entry = entries[0];
if (!entry) {
return;
}
if (entry.isIntersecting && !this.hostInsideView) {
this.hostInsideView = true;
const ellipsisConfig = this.resolveEllipsisConfig();
const mergedExpanded = this.getMergedExpanded(ellipsisConfig, this.getText());
this.emitOnViewIfNeeded(mergedExpanded);
}
},
{ threshold: 0.5 }
);
this.observer.observe(this.host);
}
emitOnViewIfNeeded(mergedExpanded) {
if (!mergedExpanded || this.viewed || !this.hostInsideView) {
return;
}
this.viewed = true;
this.props.onView?.();
}
};
var AngularContentTextWithSuggestionsRenderer = class extends AngularContentTextRenderer {
};
var AngularContentTitleWithSuggestionsRenderer = class {
renderer = new AngularContentTextWithSuggestionsRenderer();
attach(host, props = {}) {
return this.renderer.attach(host, this.normalizeProps(props));
}
update(props) {
return this.renderer.update(this.normalizeProps(props));
}
destroy() {
this.renderer.destroy();
}
getState() {
return this.renderer.getState();
}
normalizeProps(props) {
return {
...props,
weight: "bold",
blur: props.blur ?? false,
ellipsis: props.ellipsis === void 0 ? { rows: 2 } : props.ellipsis
};
}
};
exports.AngularContentSuggestionsAdapter = AngularContentSuggestionsAdapter;
exports.AngularContentTextRenderer = AngularContentTextRenderer;
exports.AngularContentTextWithSuggestionsRenderer = AngularContentTextWithSuggestionsRenderer;
exports.AngularContentTitleWithSuggestionsRenderer = AngularContentTitleWithSuggestionsRenderer;
exports.buildAngularTagHref = buildAngularTagHref;
exports.createAngularContentTokens = createAngularContentTokens;
exports.toKebabCase = toKebabCase;
//# sourceMappingURL=index.cjs.map
//# sourceMappingURL=index.cjs.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,118 @@
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[];
}
type AngularEllipsisSymbol = string | ((expanded: boolean) => string);
type AngularCountEllipsisConfig = {
count: number;
rows?: never;
expandable?: boolean;
expanded?: boolean;
symbol?: AngularEllipsisSymbol;
onExpand?: (expanded: boolean) => void;
};
type AngularRowsEllipsisConfig = {
rows: number;
count?: never;
expandable?: boolean;
expanded?: boolean;
symbol?: AngularEllipsisSymbol;
onExpand?: (expanded: boolean) => void;
};
type AngularContentEllipsisConfig = AngularCountEllipsisConfig | AngularRowsEllipsisConfig | false;
type AngularRenderResult = Node | string | number | null | undefined;
type AngularMentionRenderer = (entity: MentionEntity, index: number) => AngularRenderResult;
type AngularTagRenderer = (entity: TagEntity, index: number) => AngularRenderResult;
type AngularLinkRenderer = (entity: LinkEntity, index: number) => AngularRenderResult;
interface AngularContentTextProps {
className?: string;
weight?: "normal" | "bold";
text?: string | null;
ellipsis?: AngularContentEllipsisConfig;
blur?: boolean;
style?: Record<string, string | number> | null;
onView?: () => void;
renderMention?: AngularMentionRenderer;
renderTag?: AngularTagRenderer;
renderLink?: AngularLinkRenderer;
}
type AngularContentTextWithSuggestionsProps = Omit<AngularContentTextProps, "renderMention" | "renderTag"> & {
renderMention?: AngularMentionRenderer;
renderTag?: AngularTagRenderer;
};
type AngularContentTitleWithSuggestionsProps = Omit<AngularContentTextWithSuggestionsProps, "weight">;
interface AngularContentTextRendererState {
props: AngularContentTextProps;
snapshot: AngularContentSnapshot;
expanded: boolean;
}
declare const toKebabCase: (value: string) => string;
declare const buildAngularTagHref: (entity: TagEntity) => string;
declare const createAngularContentTokens: (inputText: string | null | undefined) => AngularContentToken[];
declare class AngularContentSuggestionsAdapter {
snapshot(inputText: string | null | undefined): AngularContentSnapshot;
}
declare class AngularContentTextRenderer {
private host;
private props;
private expanded;
private viewed;
private hostInsideView;
private observer;
attach(host: HTMLElement, props?: AngularContentTextProps): AngularContentTextRendererState;
update(nextProps: AngularContentTextProps): AngularContentTextRendererState;
destroy(): void;
getState(): AngularContentTextRendererState;
private getText;
private createSnapshot;
private resolveEllipsisConfig;
private getMergedExpanded;
private createDefaultMentionNode;
private createDefaultTagNode;
private createDefaultLinkNode;
private renderEntity;
private buildTextNodes;
private applyBaseParagraphStyle;
private buildReadMoreButton;
private handleExpand;
private computeEntitySafeCutoff;
private hasVerticalOverflow;
private render;
private initObserver;
private emitOnViewIfNeeded;
}
declare class AngularContentTextWithSuggestionsRenderer extends AngularContentTextRenderer {
}
declare class AngularContentTitleWithSuggestionsRenderer {
private readonly renderer;
attach(host: HTMLElement, props?: AngularContentTitleWithSuggestionsProps): AngularContentTextRendererState;
update(props: AngularContentTitleWithSuggestionsProps): AngularContentTextRendererState;
destroy(): void;
getState(): AngularContentTextRendererState;
private normalizeProps;
}
export { type AngularContentEllipsisConfig, type AngularContentSnapshot, AngularContentSuggestionsAdapter, type AngularContentTextProps, AngularContentTextRenderer, type AngularContentTextRendererState, type AngularContentTextWithSuggestionsProps, AngularContentTextWithSuggestionsRenderer, type AngularContentTitleWithSuggestionsProps, AngularContentTitleWithSuggestionsRenderer, type AngularContentToken, type AngularCountEllipsisConfig, type AngularEllipsisSymbol, type AngularLinkRenderer, type AngularLinkToken, type AngularMentionRenderer, type AngularMentionToken, type AngularRenderResult, type AngularRowsEllipsisConfig, type AngularTagRenderer, type AngularTagToken, type AngularTextToken, buildAngularTagHref, createAngularContentTokens, toKebabCase };

View File

@@ -0,0 +1,118 @@
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[];
}
type AngularEllipsisSymbol = string | ((expanded: boolean) => string);
type AngularCountEllipsisConfig = {
count: number;
rows?: never;
expandable?: boolean;
expanded?: boolean;
symbol?: AngularEllipsisSymbol;
onExpand?: (expanded: boolean) => void;
};
type AngularRowsEllipsisConfig = {
rows: number;
count?: never;
expandable?: boolean;
expanded?: boolean;
symbol?: AngularEllipsisSymbol;
onExpand?: (expanded: boolean) => void;
};
type AngularContentEllipsisConfig = AngularCountEllipsisConfig | AngularRowsEllipsisConfig | false;
type AngularRenderResult = Node | string | number | null | undefined;
type AngularMentionRenderer = (entity: MentionEntity, index: number) => AngularRenderResult;
type AngularTagRenderer = (entity: TagEntity, index: number) => AngularRenderResult;
type AngularLinkRenderer = (entity: LinkEntity, index: number) => AngularRenderResult;
interface AngularContentTextProps {
className?: string;
weight?: "normal" | "bold";
text?: string | null;
ellipsis?: AngularContentEllipsisConfig;
blur?: boolean;
style?: Record<string, string | number> | null;
onView?: () => void;
renderMention?: AngularMentionRenderer;
renderTag?: AngularTagRenderer;
renderLink?: AngularLinkRenderer;
}
type AngularContentTextWithSuggestionsProps = Omit<AngularContentTextProps, "renderMention" | "renderTag"> & {
renderMention?: AngularMentionRenderer;
renderTag?: AngularTagRenderer;
};
type AngularContentTitleWithSuggestionsProps = Omit<AngularContentTextWithSuggestionsProps, "weight">;
interface AngularContentTextRendererState {
props: AngularContentTextProps;
snapshot: AngularContentSnapshot;
expanded: boolean;
}
declare const toKebabCase: (value: string) => string;
declare const buildAngularTagHref: (entity: TagEntity) => string;
declare const createAngularContentTokens: (inputText: string | null | undefined) => AngularContentToken[];
declare class AngularContentSuggestionsAdapter {
snapshot(inputText: string | null | undefined): AngularContentSnapshot;
}
declare class AngularContentTextRenderer {
private host;
private props;
private expanded;
private viewed;
private hostInsideView;
private observer;
attach(host: HTMLElement, props?: AngularContentTextProps): AngularContentTextRendererState;
update(nextProps: AngularContentTextProps): AngularContentTextRendererState;
destroy(): void;
getState(): AngularContentTextRendererState;
private getText;
private createSnapshot;
private resolveEllipsisConfig;
private getMergedExpanded;
private createDefaultMentionNode;
private createDefaultTagNode;
private createDefaultLinkNode;
private renderEntity;
private buildTextNodes;
private applyBaseParagraphStyle;
private buildReadMoreButton;
private handleExpand;
private computeEntitySafeCutoff;
private hasVerticalOverflow;
private render;
private initObserver;
private emitOnViewIfNeeded;
}
declare class AngularContentTextWithSuggestionsRenderer extends AngularContentTextRenderer {
}
declare class AngularContentTitleWithSuggestionsRenderer {
private readonly renderer;
attach(host: HTMLElement, props?: AngularContentTitleWithSuggestionsProps): AngularContentTextRendererState;
update(props: AngularContentTitleWithSuggestionsProps): AngularContentTextRendererState;
destroy(): void;
getState(): AngularContentTextRendererState;
private normalizeProps;
}
export { type AngularContentEllipsisConfig, type AngularContentSnapshot, AngularContentSuggestionsAdapter, type AngularContentTextProps, AngularContentTextRenderer, type AngularContentTextRendererState, type AngularContentTextWithSuggestionsProps, AngularContentTextWithSuggestionsRenderer, type AngularContentTitleWithSuggestionsProps, AngularContentTitleWithSuggestionsRenderer, type AngularContentToken, type AngularCountEllipsisConfig, type AngularEllipsisSymbol, type AngularLinkRenderer, type AngularLinkToken, type AngularMentionRenderer, type AngularMentionToken, type AngularRenderResult, type AngularRowsEllipsisConfig, type AngularTagRenderer, type AngularTagToken, type AngularTextToken, buildAngularTagHref, createAngularContentTokens, toKebabCase };

View File

@@ -0,0 +1,487 @@
// 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 LINK_COLOR = "#1677ff";
var READ_MORE_TEXT = "\u0427\u0438\u0442\u0430\u0442\u044C \u043F\u043E\u043B\u043D\u043E\u0441\u0442\u044C\u044E";
var toKebabCase = (value) => value.replace(/[A-Z]/g, (char) => `-${char.toLowerCase()}`);
var applyStyleObject = (element, styles) => {
if (!styles) {
return;
}
for (const [key, rawValue] of Object.entries(styles)) {
if (rawValue === null || rawValue === void 0) {
continue;
}
const styleValue = typeof rawValue === "number" ? `${rawValue}px` : String(rawValue);
if (key.startsWith("--")) {
element.style.setProperty(key, styleValue);
continue;
}
const cssKey = key.includes("-") ? key : toKebabCase(key);
element.style.setProperty(cssKey, styleValue);
}
};
var toNode = (value) => {
if (value === null || value === void 0) {
return null;
}
if (value instanceof Node) {
return value;
}
return document.createTextNode(String(value));
};
var resolveEllipsisSymbol = (symbol, expanded) => {
if (typeof symbol === "function") {
return symbol(expanded);
}
return symbol ?? READ_MORE_TEXT;
};
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
};
}
};
var AngularContentTextRenderer = class {
host = null;
props = {};
expanded = false;
viewed = false;
hostInsideView = false;
observer = null;
attach(host, props = {}) {
this.destroy();
this.host = host;
this.props = { ...props };
this.expanded = false;
this.viewed = false;
this.hostInsideView = false;
this.initObserver();
this.render();
return this.getState();
}
update(nextProps) {
this.props = {
...this.props,
...nextProps
};
this.render();
return this.getState();
}
destroy() {
if (this.observer && this.host) {
this.observer.unobserve(this.host);
this.observer.disconnect();
}
this.observer = null;
if (this.host) {
this.host.innerHTML = "";
}
this.host = null;
this.props = {};
this.expanded = false;
this.viewed = false;
this.hostInsideView = false;
}
getState() {
return {
props: { ...this.props },
snapshot: this.createSnapshot(),
expanded: this.getMergedExpanded(this.resolveEllipsisConfig(), this.getText())
};
}
getText() {
return this.props.text ?? "";
}
createSnapshot() {
const text = this.getText();
const entities = findAllEntities(text);
const tokens = createAngularContentTokens(text);
return {
text,
entities,
tokens
};
}
resolveEllipsisConfig() {
const ellipsis = this.props.ellipsis;
if (!ellipsis || typeof ellipsis !== "object") {
return null;
}
return ellipsis;
}
getMergedExpanded(ellipsisConfig, text) {
const controlledExpanded = ellipsisConfig && typeof ellipsisConfig.expanded === "boolean" ? ellipsisConfig.expanded : void 0;
if (controlledExpanded !== void 0) {
return controlledExpanded;
}
if (ellipsisConfig && "count" in ellipsisConfig) {
const count = ellipsisConfig.count ?? 0;
if (count <= 0 || text.length <= count) {
return true;
}
}
return this.expanded;
}
createDefaultMentionNode(entity) {
const span = document.createElement("span");
span.style.color = LINK_COLOR;
span.style.fontWeight = this.props.weight === "bold" ? "700" : "400";
span.textContent = entity.displayText;
return span;
}
createDefaultTagNode(entity) {
const span = document.createElement("span");
span.style.color = LINK_COLOR;
span.style.fontWeight = this.props.weight === "bold" ? "700" : "400";
span.textContent = entity.text;
return span;
}
createDefaultLinkNode(entity) {
const anchor = document.createElement("a");
anchor.href = entity.url;
anchor.target = "_blank";
anchor.referrerPolicy = "no-referrer";
anchor.style.color = LINK_COLOR;
anchor.style.fontWeight = this.props.weight === "bold" ? "700" : "400";
anchor.textContent = entity.text;
return anchor;
}
renderEntity(entity, index) {
if (entity.type === "mention") {
const customNode2 = this.props.renderMention?.(entity, index);
return toNode(customNode2) ?? this.createDefaultMentionNode(entity);
}
if (entity.type === "tag") {
const customNode2 = this.props.renderTag?.(entity, index);
return toNode(customNode2) ?? this.createDefaultTagNode(entity);
}
const customNode = this.props.renderLink?.(entity, index);
return toNode(customNode) ?? this.createDefaultLinkNode(entity);
}
buildTextNodes(text, entities, upto = null) {
let lastIndex = 0;
const nodes = [];
for (const [index, 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(document.createTextNode(text.slice(lastIndex, textEnd)));
}
if (upto === null || entity.end <= upto) {
const entityNode = this.renderEntity(entity, index);
if (entityNode) {
nodes.push(entityNode);
}
}
lastIndex = entity.end;
}
if (upto === null) {
if (lastIndex < text.length) {
nodes.push(document.createTextNode(text.slice(lastIndex)));
}
} else if (lastIndex < upto) {
nodes.push(document.createTextNode(text.slice(lastIndex, upto)));
}
return nodes;
}
applyBaseParagraphStyle(element) {
element.style.whiteSpace = "pre-wrap";
element.style.fontWeight = this.props.weight === "bold" ? "700" : "400";
element.style.setProperty("-webkit-touch-callout", "default");
element.style.setProperty("-webkit-user-select", "text");
element.style.setProperty("-khtml-user-select", "text");
element.style.setProperty("-moz-user-select", "text");
element.style.setProperty("-ms-user-select", "text");
element.style.setProperty("user-select", "text");
if (this.props.blur) {
element.style.filter = "blur(3px)";
element.style.setProperty("-webkit-user-select", "none");
element.style.setProperty("-khtml-user-select", "none");
element.style.setProperty("-moz-user-select", "none");
element.style.setProperty("-ms-user-select", "none");
element.style.setProperty("user-select", "none");
element.style.pointerEvents = "none";
}
applyStyleObject(element, this.props.style);
}
buildReadMoreButton(symbol) {
const button = document.createElement("button");
button.type = "button";
button.textContent = resolveEllipsisSymbol(symbol, this.expanded);
button.style.border = "0";
button.style.background = "none";
button.style.padding = "0";
button.style.marginLeft = "6px";
button.style.cursor = "pointer";
button.style.color = LINK_COLOR;
button.style.fontWeight = "700";
button.onclick = (event) => {
event.preventDefault();
event.stopPropagation();
this.handleExpand();
};
return button;
}
handleExpand() {
const ellipsisConfig = this.resolveEllipsisConfig();
if (!ellipsisConfig) {
return;
}
this.expanded = true;
ellipsisConfig.onExpand?.(true);
this.render();
}
computeEntitySafeCutoff(count, entities) {
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;
}
}
}
return cutoff;
}
hasVerticalOverflow(element) {
return element.scrollHeight - element.clientHeight > 1;
}
render() {
if (!this.host) {
return;
}
const text = this.getText();
const entities = findAllEntities(text);
const ellipsisConfig = this.resolveEllipsisConfig();
const mergedExpanded = this.getMergedExpanded(ellipsisConfig, text);
const wrapper = document.createElement("div");
const paragraph = document.createElement("div");
if (this.props.className) {
paragraph.className = this.props.className;
}
this.applyBaseParagraphStyle(paragraph);
if (ellipsisConfig && "count" in ellipsisConfig) {
const count = ellipsisConfig.count ?? 0;
const shouldTruncate = !mergedExpanded && count > 0 && text.length > count;
if (shouldTruncate) {
const cutoff = this.computeEntitySafeCutoff(count, entities);
const nodes = this.buildTextNodes(text, entities, cutoff);
paragraph.append(...nodes);
paragraph.append(document.createTextNode("\u2026"));
if (ellipsisConfig.expandable) {
paragraph.append(this.buildReadMoreButton(ellipsisConfig.symbol));
}
} else {
paragraph.append(...this.buildTextNodes(text, entities));
}
} else {
paragraph.append(...this.buildTextNodes(text, entities));
if (ellipsisConfig && "rows" in ellipsisConfig && !mergedExpanded) {
paragraph.style.display = "-webkit-box";
paragraph.style.setProperty("-webkit-box-orient", "vertical");
paragraph.style.setProperty("-webkit-line-clamp", String(ellipsisConfig.rows));
paragraph.style.overflow = "hidden";
wrapper.append(paragraph);
this.host.replaceChildren(wrapper);
if (ellipsisConfig.expandable && this.hasVerticalOverflow(paragraph)) {
wrapper.append(this.buildReadMoreButton(ellipsisConfig.symbol));
}
this.emitOnViewIfNeeded(mergedExpanded);
return;
}
}
wrapper.append(paragraph);
this.host.replaceChildren(wrapper);
this.emitOnViewIfNeeded(mergedExpanded);
}
initObserver() {
if (!this.host) {
return;
}
if (typeof IntersectionObserver === "undefined") {
this.hostInsideView = true;
return;
}
this.observer = new IntersectionObserver(
(entries) => {
const entry = entries[0];
if (!entry) {
return;
}
if (entry.isIntersecting && !this.hostInsideView) {
this.hostInsideView = true;
const ellipsisConfig = this.resolveEllipsisConfig();
const mergedExpanded = this.getMergedExpanded(ellipsisConfig, this.getText());
this.emitOnViewIfNeeded(mergedExpanded);
}
},
{ threshold: 0.5 }
);
this.observer.observe(this.host);
}
emitOnViewIfNeeded(mergedExpanded) {
if (!mergedExpanded || this.viewed || !this.hostInsideView) {
return;
}
this.viewed = true;
this.props.onView?.();
}
};
var AngularContentTextWithSuggestionsRenderer = class extends AngularContentTextRenderer {
};
var AngularContentTitleWithSuggestionsRenderer = class {
renderer = new AngularContentTextWithSuggestionsRenderer();
attach(host, props = {}) {
return this.renderer.attach(host, this.normalizeProps(props));
}
update(props) {
return this.renderer.update(this.normalizeProps(props));
}
destroy() {
this.renderer.destroy();
}
getState() {
return this.renderer.getState();
}
normalizeProps(props) {
return {
...props,
weight: "bold",
blur: props.blur ?? false,
ellipsis: props.ellipsis === void 0 ? { rows: 2 } : props.ellipsis
};
}
};
export { AngularContentSuggestionsAdapter, AngularContentTextRenderer, AngularContentTextWithSuggestionsRenderer, AngularContentTitleWithSuggestionsRenderer, buildAngularTagHref, createAngularContentTokens, toKebabCase };
//# 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.2",
"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,650 @@
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 type AngularEllipsisSymbol = string | ((expanded: boolean) => string);
export type AngularCountEllipsisConfig = {
count: number;
rows?: never;
expandable?: boolean;
expanded?: boolean;
symbol?: AngularEllipsisSymbol;
onExpand?: (expanded: boolean) => void;
};
export type AngularRowsEllipsisConfig = {
rows: number;
count?: never;
expandable?: boolean;
expanded?: boolean;
symbol?: AngularEllipsisSymbol;
onExpand?: (expanded: boolean) => void;
};
export type AngularContentEllipsisConfig =
| AngularCountEllipsisConfig
| AngularRowsEllipsisConfig
| false;
export type AngularRenderResult = Node | string | number | null | undefined;
export type AngularMentionRenderer = (
entity: MentionEntity,
index: number,
) => AngularRenderResult;
export type AngularTagRenderer = (
entity: TagEntity,
index: number,
) => AngularRenderResult;
export type AngularLinkRenderer = (
entity: LinkEntity,
index: number,
) => AngularRenderResult;
export interface AngularContentTextProps {
className?: string;
weight?: "normal" | "bold";
text?: string | null;
ellipsis?: AngularContentEllipsisConfig;
blur?: boolean;
style?: Record<string, string | number> | null;
onView?: () => void;
renderMention?: AngularMentionRenderer;
renderTag?: AngularTagRenderer;
renderLink?: AngularLinkRenderer;
}
export type AngularContentTextWithSuggestionsProps = Omit<
AngularContentTextProps,
"renderMention" | "renderTag"
> & {
renderMention?: AngularMentionRenderer;
renderTag?: AngularTagRenderer;
};
export type AngularContentTitleWithSuggestionsProps = Omit<
AngularContentTextWithSuggestionsProps,
"weight"
>;
export interface AngularContentTextRendererState {
props: AngularContentTextProps;
snapshot: AngularContentSnapshot;
expanded: boolean;
}
const LINK_COLOR = "#1677ff";
const READ_MORE_TEXT = "Читать полностью";
const toKebabCase = (value: string) => value.replace(/[A-Z]/g, char => `-${char.toLowerCase()}`);
const applyStyleObject = (
element: HTMLElement,
styles?: Record<string, string | number> | null,
) => {
if (!styles) {
return;
}
for (const [key, rawValue] of Object.entries(styles)) {
if (rawValue === null || rawValue === undefined) {
continue;
}
const styleValue =
typeof rawValue === "number" ? `${rawValue}px` : String(rawValue);
if (key.startsWith("--")) {
element.style.setProperty(key, styleValue);
continue;
}
const cssKey = key.includes("-") ? key : toKebabCase(key);
element.style.setProperty(cssKey, styleValue);
}
};
const toNode = (value: AngularRenderResult): Node | null => {
if (value === null || value === undefined) {
return null;
}
if (value instanceof Node) {
return value;
}
return document.createTextNode(String(value));
};
const resolveEllipsisSymbol = (
symbol: AngularEllipsisSymbol | undefined,
expanded: boolean,
): string => {
if (typeof symbol === "function") {
return symbol(expanded);
}
return symbol ?? READ_MORE_TEXT;
};
const buildAngularTagHref = (entity: TagEntity): string => {
return `/search/?query=${encodeURIComponent(entity.tag.toLowerCase())}`;
};
export { buildAngularTagHref };
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,
};
}
}
export class AngularContentTextRenderer {
private host: HTMLElement | null = null;
private props: AngularContentTextProps = {};
private expanded = false;
private viewed = false;
private hostInsideView = false;
private observer: IntersectionObserver | null = null;
attach(host: HTMLElement, props: AngularContentTextProps = {}): AngularContentTextRendererState {
this.destroy();
this.host = host;
this.props = { ...props };
this.expanded = false;
this.viewed = false;
this.hostInsideView = false;
this.initObserver();
this.render();
return this.getState();
}
update(nextProps: AngularContentTextProps): AngularContentTextRendererState {
this.props = {
...this.props,
...nextProps,
};
this.render();
return this.getState();
}
destroy(): void {
if (this.observer && this.host) {
this.observer.unobserve(this.host);
this.observer.disconnect();
}
this.observer = null;
if (this.host) {
this.host.innerHTML = "";
}
this.host = null;
this.props = {};
this.expanded = false;
this.viewed = false;
this.hostInsideView = false;
}
getState(): AngularContentTextRendererState {
return {
props: { ...this.props },
snapshot: this.createSnapshot(),
expanded: this.getMergedExpanded(this.resolveEllipsisConfig(), this.getText()),
};
}
private getText(): string {
return this.props.text ?? "";
}
private createSnapshot(): AngularContentSnapshot {
const text = this.getText();
const entities = findAllEntities(text);
const tokens = createAngularContentTokens(text);
return {
text,
entities,
tokens,
};
}
private resolveEllipsisConfig():
| AngularCountEllipsisConfig
| AngularRowsEllipsisConfig
| null {
const ellipsis = this.props.ellipsis;
if (!ellipsis || typeof ellipsis !== "object") {
return null;
}
return ellipsis;
}
private getMergedExpanded(
ellipsisConfig: AngularCountEllipsisConfig | AngularRowsEllipsisConfig | null,
text: string,
): boolean {
const controlledExpanded =
ellipsisConfig && typeof ellipsisConfig.expanded === "boolean"
? ellipsisConfig.expanded
: undefined;
if (controlledExpanded !== undefined) {
return controlledExpanded;
}
if (ellipsisConfig && "count" in ellipsisConfig) {
const count = ellipsisConfig.count ?? 0;
if (count <= 0 || text.length <= count) {
return true;
}
}
return this.expanded;
}
private createDefaultMentionNode(entity: MentionEntity): Node {
const span = document.createElement("span");
span.style.color = LINK_COLOR;
span.style.fontWeight = this.props.weight === "bold" ? "700" : "400";
span.textContent = entity.displayText;
return span;
}
private createDefaultTagNode(entity: TagEntity): Node {
const span = document.createElement("span");
span.style.color = LINK_COLOR;
span.style.fontWeight = this.props.weight === "bold" ? "700" : "400";
span.textContent = entity.text;
return span;
}
private createDefaultLinkNode(entity: LinkEntity): Node {
const anchor = document.createElement("a");
anchor.href = entity.url;
anchor.target = "_blank";
anchor.referrerPolicy = "no-referrer";
anchor.style.color = LINK_COLOR;
anchor.style.fontWeight = this.props.weight === "bold" ? "700" : "400";
anchor.textContent = entity.text;
return anchor;
}
private renderEntity(entity: ContentEntity, index: number): Node | null {
if (entity.type === "mention") {
const customNode = this.props.renderMention?.(entity, index);
return toNode(customNode) ?? this.createDefaultMentionNode(entity);
}
if (entity.type === "tag") {
const customNode = this.props.renderTag?.(entity, index);
return toNode(customNode) ?? this.createDefaultTagNode(entity);
}
const customNode = this.props.renderLink?.(entity, index);
return toNode(customNode) ?? this.createDefaultLinkNode(entity);
}
private buildTextNodes(
text: string,
entities: ContentEntity[],
upto: number | null = null,
): Node[] {
let lastIndex = 0;
const nodes: Node[] = [];
for (const [index, 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(document.createTextNode(text.slice(lastIndex, textEnd)));
}
if (upto === null || entity.end <= upto) {
const entityNode = this.renderEntity(entity, index);
if (entityNode) {
nodes.push(entityNode);
}
}
lastIndex = entity.end;
}
if (upto === null) {
if (lastIndex < text.length) {
nodes.push(document.createTextNode(text.slice(lastIndex)));
}
} else if (lastIndex < upto) {
nodes.push(document.createTextNode(text.slice(lastIndex, upto)));
}
return nodes;
}
private applyBaseParagraphStyle(element: HTMLElement): void {
element.style.whiteSpace = "pre-wrap";
element.style.fontWeight = this.props.weight === "bold" ? "700" : "400";
element.style.setProperty("-webkit-touch-callout", "default");
element.style.setProperty("-webkit-user-select", "text");
element.style.setProperty("-khtml-user-select", "text");
element.style.setProperty("-moz-user-select", "text");
element.style.setProperty("-ms-user-select", "text");
element.style.setProperty("user-select", "text");
if (this.props.blur) {
element.style.filter = "blur(3px)";
element.style.setProperty("-webkit-user-select", "none");
element.style.setProperty("-khtml-user-select", "none");
element.style.setProperty("-moz-user-select", "none");
element.style.setProperty("-ms-user-select", "none");
element.style.setProperty("user-select", "none");
element.style.pointerEvents = "none";
}
applyStyleObject(element, this.props.style);
}
private buildReadMoreButton(symbol: AngularEllipsisSymbol | undefined): HTMLButtonElement {
const button = document.createElement("button");
button.type = "button";
button.textContent = resolveEllipsisSymbol(symbol, this.expanded);
button.style.border = "0";
button.style.background = "none";
button.style.padding = "0";
button.style.marginLeft = "6px";
button.style.cursor = "pointer";
button.style.color = LINK_COLOR;
button.style.fontWeight = "700";
button.onclick = event => {
event.preventDefault();
event.stopPropagation();
this.handleExpand();
};
return button;
}
private handleExpand(): void {
const ellipsisConfig = this.resolveEllipsisConfig();
if (!ellipsisConfig) {
return;
}
this.expanded = true;
ellipsisConfig.onExpand?.(true);
this.render();
}
private computeEntitySafeCutoff(
count: number,
entities: ContentEntity[],
): number {
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;
}
}
}
return cutoff;
}
private hasVerticalOverflow(element: HTMLElement): boolean {
return element.scrollHeight - element.clientHeight > 1;
}
private render(): void {
if (!this.host) {
return;
}
const text = this.getText();
const entities = findAllEntities(text);
const ellipsisConfig = this.resolveEllipsisConfig();
const mergedExpanded = this.getMergedExpanded(ellipsisConfig, text);
const wrapper = document.createElement("div");
const paragraph = document.createElement("div");
if (this.props.className) {
paragraph.className = this.props.className;
}
this.applyBaseParagraphStyle(paragraph);
if (ellipsisConfig && "count" in ellipsisConfig) {
const count = ellipsisConfig.count ?? 0;
const shouldTruncate = !mergedExpanded && count > 0 && text.length > count;
if (shouldTruncate) {
const cutoff = this.computeEntitySafeCutoff(count, entities);
const nodes = this.buildTextNodes(text, entities, cutoff);
paragraph.append(...nodes);
paragraph.append(document.createTextNode("…"));
if (ellipsisConfig.expandable) {
paragraph.append(this.buildReadMoreButton(ellipsisConfig.symbol));
}
} else {
paragraph.append(...this.buildTextNodes(text, entities));
}
} else {
paragraph.append(...this.buildTextNodes(text, entities));
if (ellipsisConfig && "rows" in ellipsisConfig && !mergedExpanded) {
paragraph.style.display = "-webkit-box";
paragraph.style.setProperty("-webkit-box-orient", "vertical");
paragraph.style.setProperty("-webkit-line-clamp", String(ellipsisConfig.rows));
paragraph.style.overflow = "hidden";
// Важно: сначала рендерим paragraph в DOM, потом меряем реальный overflow.
wrapper.append(paragraph);
this.host.replaceChildren(wrapper);
if (ellipsisConfig.expandable && this.hasVerticalOverflow(paragraph)) {
wrapper.append(this.buildReadMoreButton(ellipsisConfig.symbol));
}
this.emitOnViewIfNeeded(mergedExpanded);
return;
}
}
wrapper.append(paragraph);
this.host.replaceChildren(wrapper);
this.emitOnViewIfNeeded(mergedExpanded);
}
private initObserver(): void {
if (!this.host) {
return;
}
if (typeof IntersectionObserver === "undefined") {
this.hostInsideView = true;
return;
}
this.observer = new IntersectionObserver(
entries => {
const entry = entries[0];
if (!entry) {
return;
}
if (entry.isIntersecting && !this.hostInsideView) {
this.hostInsideView = true;
const ellipsisConfig = this.resolveEllipsisConfig();
const mergedExpanded = this.getMergedExpanded(ellipsisConfig, this.getText());
this.emitOnViewIfNeeded(mergedExpanded);
}
},
{ threshold: 0.5 },
);
this.observer.observe(this.host);
}
private emitOnViewIfNeeded(mergedExpanded: boolean): void {
if (!mergedExpanded || this.viewed || !this.hostInsideView) {
return;
}
this.viewed = true;
this.props.onView?.();
}
}
export class AngularContentTextWithSuggestionsRenderer extends AngularContentTextRenderer {}
export class AngularContentTitleWithSuggestionsRenderer {
private readonly renderer = new AngularContentTextWithSuggestionsRenderer();
attach(
host: HTMLElement,
props: AngularContentTitleWithSuggestionsProps = {},
): AngularContentTextRendererState {
return this.renderer.attach(host, this.normalizeProps(props));
}
update(props: AngularContentTitleWithSuggestionsProps): AngularContentTextRendererState {
return this.renderer.update(this.normalizeProps(props));
}
destroy(): void {
this.renderer.destroy();
}
getState(): AngularContentTextRendererState {
return this.renderer.getState();
}
private normalizeProps(
props: AngularContentTitleWithSuggestionsProps,
): AngularContentTextWithSuggestionsProps {
return {
...props,
weight: "bold",
blur: props.blur ?? false,
ellipsis: props.ellipsis === undefined ? { rows: 2 } : props.ellipsis,
};
}
}
export { toKebabCase };

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",
],
});

View File

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

View File

@@ -0,0 +1,90 @@
:root {
--Text-Primary: #0f172a;
--Text-Secondary: #334155;
--Text-Tertiary: #64748b;
--Text-Quaternary: #94a3b8;
--System-HashtagsInPost: #2563eb;
--Default-White: #ffffff;
--Default-Dark: #020617;
--System-Alert: #dc2626;
--Accent-Malahit: #16a34a;
--System-Attantion: #d97706;
}
body {
font-family: Inter, "Segoe UI", sans-serif;
}
.sb-show-main {
padding: 24px;
}
.tach-story-surface {
min-width: 320px;
max-width: 920px;
padding: 20px;
border-radius: 14px;
border: 1px solid #dbe1ea;
background: #ffffff;
}
.tach-story-stack {
display: flex;
flex-direction: column;
gap: 12px;
}
.tach-story-grid {
display: grid;
gap: 10px;
}
.tach-story-grid--colors {
grid-template-columns: repeat(auto-fill, minmax(210px, 1fr));
}
.tach-story-row {
display: grid;
gap: 12px;
grid-template-columns: 220px minmax(0, 1fr);
align-items: center;
padding: 10px 12px;
border-radius: 10px;
border: 1px solid #e2e8f0;
background: #fff;
}
.tach-story-label {
color: #475569;
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
font-size: 12px;
}
.tach-story-token-list {
margin: 0;
padding-left: 18px;
color: #334155;
}
.tach-story-table {
width: 100%;
border-collapse: collapse;
font-size: 13px;
}
.tach-story-table th,
.tach-story-table td {
padding: 8px 10px;
border: 1px solid #e2e8f0;
text-align: left;
}
.tach-story-table thead {
background: #f8fafc;
}
@media (max-width: 720px) {
.tach-story-row {
grid-template-columns: 1fr;
}
}

View File

@@ -0,0 +1,27 @@
import type { Preview } from "@storybook/react";
import "antd/dist/reset.css";
import "../src/styles/tach-typography.css";
import "./preview.css";
const preview: Preview = {
parameters: {
layout: "centered",
controls: {
expanded: true,
sort: "requiredFirst",
},
actions: {
argTypesRegex: "^on[A-Z].*",
},
backgrounds: {
default: "surface",
values: [
{ name: "surface", value: "#F5F7FB" },
{ name: "dark", value: "#0F172A" },
],
},
},
};
export default preview;

View File

@@ -0,0 +1,107 @@
# @hublib-web/tach-typography
Typography package with shared tokens and framework adapters:
- `react` adapter based on `antd/Typography`
- `angular` adapter based on `ng-zorro-antd/typography`
## Install from Git (SSH tag)
```bash
yarn add "@hublib-web/tach-typography@git+ssh://git@github.com/ORG/REPO.git#workspace=@hublib-web/tach-typography&tag=tach-typography-v0.2.0"
```
## Install inside this monorepo
```bash
yarn add @hublib-web/tach-typography
```
## Release this package
1. Bump `version` in `packages/tach-typography/package.json`.
2. Build package artifacts:
```bash
yarn workspace @hublib-web/tach-typography build
```
3. Commit release files:
```bash
git add packages/tach-typography/package.json packages/tach-typography/dist
git commit -m "release(tach-typography): v0.2.0"
```
4. Create and push tag:
```bash
git tag -a tach-typography-v0.2.0 -m "@hublib-web/tach-typography v0.2.0"
git push origin main --follow-tags
```
Detailed docs:
- [Release policy](../../docs/release-policy.md)
- [Git installation](../../docs/git-installation.md)
## React usage (Ant Design)
```tsx
import "@hublib-web/tach-typography/styles.css";
import { TachTypography } from "@hublib-web/tach-typography/react";
export const Example = () => (
<TachTypography.Text.Body color="link" weight="bold" ellipsis={{ rows: 1 }}>
Hello from React + AntD
</TachTypography.Text.Body>
);
```
## Angular usage (NG-ZORRO)
```ts
import { Component } from "@angular/core";
import { TachTypographyComponent } from "@hublib-web/tach-typography/angular";
@Component({
selector: "app-example",
standalone: true,
imports: [TachTypographyComponent],
template: `
<tach-typography
variant="Body"
color="link"
weight="bold"
[nzProps]="{ nzCopyable: true, nzType: 'secondary' }"
>
Hello from Angular + NG-ZORRO
</tach-typography>
<tach-typography
as="a"
variant="Body"
color="link"
[hostProps]="{ href: '/docs', target: '_blank', rel: 'noopener noreferrer' }"
[nzProps]="{ nzType: 'secondary' }"
>
Open docs
</tach-typography>
`,
})
export class ExampleComponent {}
```
## Storybook (dev/design system)
Run from repository root:
```bash
yarn workspace @hublib-web/tach-typography storybook
```
Build static Storybook:
```bash
yarn workspace @hublib-web/tach-typography storybook:build
```

View File

@@ -0,0 +1,81 @@
import { ElementRef, EventEmitter, OnChanges, Renderer2, SimpleChanges } from "@angular/core";
import { NzTypographyComponent } from "ng-zorro-antd/typography";
import { type EllipsisOptions, type TypographyClassOptions, type TypographyColor, type TypographyRenderOptions, type TypographyVariant, type TypographyWeight } from "../core";
import * as i0 from "@angular/core";
import * as i1 from "ng-zorro-antd/typography";
export type AngularTypographyClassInput = TypographyClassOptions;
export interface AngularTypographyRenderOptions extends TypographyRenderOptions {
preserveStyle?: Record<string, string | number>;
}
export type TachTypographyHostTag = "span" | "p" | "a" | "h1" | "h2" | "h3" | "h4";
type NonFunctionNonEmitterKeys<T> = {
[K in keyof T]-?: T[K] extends (...args: never[]) => unknown ? never : T[K] extends EventEmitter<unknown> ? never : K;
}[keyof T];
type NzTypographyInputKey = Extract<NonFunctionNonEmitterKeys<NzTypographyComponent>, `nz${string}`>;
export type TachTypographyNzProps = Partial<Pick<NzTypographyComponent, NzTypographyInputKey>>;
export type TachTypographyHostProps = Record<string, unknown>;
export declare const tachAngularTypographyClassName: (options?: AngularTypographyClassInput) => string;
export declare const tachAngularTypographyClassList: (options?: AngularTypographyClassInput) => string[];
export declare const tachAngularTypographyStyles: (ellipsis?: EllipsisOptions, preserveStyle?: Record<string, string | number>) => Record<string, string | number>;
export declare class TachTypographyDirective implements OnChanges {
private readonly elementRef;
private readonly renderer;
tachTypography: TypographyVariant | "" | undefined;
tachTypographyVariant: TypographyVariant;
tachTypographyColor: TypographyColor;
tachTypographyWeight: TypographyWeight;
tachTypographyClickable: boolean;
tachTypographyClassName: string | undefined;
tachTypographyEllipsis: EllipsisOptions | undefined;
private readonly appliedClasses;
private readonly appliedStyleProperties;
constructor(elementRef: ElementRef<HTMLElement>, renderer: Renderer2);
ngOnChanges(_changes: SimpleChanges): void;
private syncClasses;
private syncEllipsisStyles;
static ɵfac: i0.ɵɵFactoryDeclaration<TachTypographyDirective, never>;
static ɵdir: i0.ɵɵDirectiveDeclaration<TachTypographyDirective, "[tachTypography]", never, { "tachTypography": { "alias": "tachTypography"; "required": false; }; "tachTypographyVariant": { "alias": "tachTypographyVariant"; "required": false; }; "tachTypographyColor": { "alias": "tachTypographyColor"; "required": false; }; "tachTypographyWeight": { "alias": "tachTypographyWeight"; "required": false; }; "tachTypographyClickable": { "alias": "tachTypographyClickable"; "required": false; }; "tachTypographyClassName": { "alias": "tachTypographyClassName"; "required": false; }; "tachTypographyEllipsis": { "alias": "tachTypographyEllipsis"; "required": false; }; }, {}, never, never, true, never>;
}
export declare class TachTypographyNzPropsDirective implements OnChanges {
tachTypographyNzProps: TachTypographyNzProps | null | undefined;
private readonly appliedNzKeys;
private readonly nzTypography;
ngOnChanges(): void;
static ɵfac: i0.ɵɵFactoryDeclaration<TachTypographyNzPropsDirective, never>;
static ɵdir: i0.ɵɵDirectiveDeclaration<TachTypographyNzPropsDirective, "[tachTypographyNzProps]", never, { "tachTypographyNzProps": { "alias": "tachTypographyNzProps"; "required": false; }; }, {}, never, never, true, never>;
}
export declare class TachTypographyHostPropsDirective implements OnChanges {
private readonly elementRef;
private readonly renderer;
tachTypographyHostProps: TachTypographyHostProps | null | undefined;
private readonly appliedHostProps;
constructor(elementRef: ElementRef<HTMLElement>, renderer: Renderer2);
ngOnChanges(): void;
private shouldApplyAsAttribute;
private hasPropertyOnElement;
static ɵfac: i0.ɵɵFactoryDeclaration<TachTypographyHostPropsDirective, never>;
static ɵdir: i0.ɵɵDirectiveDeclaration<TachTypographyHostPropsDirective, "[tachTypographyHostProps]", never, { "tachTypographyHostProps": { "alias": "tachTypographyHostProps"; "required": false; }; }, {}, never, never, true, never>;
}
export declare class TachTypographyComponent {
hostTag: TachTypographyHostTag;
variant: TypographyVariant;
color: TypographyColor;
weight: TypographyWeight;
clickable: boolean;
className: string | undefined;
ellipsis: EllipsisOptions | undefined;
nzProps: TachTypographyNzProps | undefined;
hostProps: TachTypographyHostProps | undefined;
preserveStyle: Record<string, string | number> | undefined;
readonly tachClick: EventEmitter<MouseEvent>;
handleClick(event: MouseEvent): void;
static ɵfac: i0.ɵɵFactoryDeclaration<TachTypographyComponent, never>;
static ɵcmp: i0.ɵɵComponentDeclaration<TachTypographyComponent, "tach-typography", never, { "hostTag": { "alias": "as"; "required": false; }; "variant": { "alias": "variant"; "required": false; }; "color": { "alias": "color"; "required": false; }; "weight": { "alias": "weight"; "required": false; }; "clickable": { "alias": "clickable"; "required": false; }; "className": { "alias": "className"; "required": false; }; "ellipsis": { "alias": "ellipsis"; "required": false; }; "nzProps": { "alias": "nzProps"; "required": false; }; "hostProps": { "alias": "hostProps"; "required": false; }; "preserveStyle": { "alias": "preserveStyle"; "required": false; }; }, { "tachClick": "tachClick"; }, never, ["*", "*", "*", "*", "*", "*", "*"], true, never>;
}
export declare class TachTypographyNzModule {
static ɵfac: i0.ɵɵFactoryDeclaration<TachTypographyNzModule, never>;
static ɵmod: i0.ɵɵNgModuleDeclaration<TachTypographyNzModule, never, [typeof i1.NzTypographyModule, typeof TachTypographyDirective, typeof TachTypographyNzPropsDirective, typeof TachTypographyHostPropsDirective, typeof TachTypographyComponent], [typeof i1.NzTypographyModule, typeof TachTypographyDirective, typeof TachTypographyNzPropsDirective, typeof TachTypographyHostPropsDirective, typeof TachTypographyComponent]>;
static ɵinj: i0.ɵɵInjectorDeclaration<TachTypographyNzModule>;
}
export {};
//# sourceMappingURL=index.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/angular/index.ts"],"names":[],"mappings":"AACA,OAAO,EAIL,UAAU,EACV,YAAY,EAIZ,SAAS,EAET,SAAS,EACT,aAAa,EACd,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,qBAAqB,EAAsB,MAAM,0BAA0B,CAAC;AAErF,OAAO,EAIL,KAAK,eAAe,EACpB,KAAK,sBAAsB,EAC3B,KAAK,eAAe,EACpB,KAAK,uBAAuB,EAC5B,KAAK,iBAAiB,EACtB,KAAK,gBAAgB,EACtB,MAAM,SAAS,CAAC;;;AAEjB,MAAM,MAAM,2BAA2B,GAAG,sBAAsB,CAAC;AAEjE,MAAM,WAAW,8BAA+B,SAAQ,uBAAuB;IAC7E,aAAa,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,CAAC;CACjD;AAED,MAAM,MAAM,qBAAqB,GAAG,MAAM,GAAG,GAAG,GAAG,GAAG,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC;AACnF,KAAK,yBAAyB,CAAC,CAAC,IAAI;KACjC,CAAC,IAAI,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,GAAG,IAAI,EAAE,KAAK,EAAE,KAAK,OAAO,GACxD,KAAK,GACL,CAAC,CAAC,CAAC,CAAC,SAAS,YAAY,CAAC,OAAO,CAAC,GAChC,KAAK,GACL,CAAC;CACR,CAAC,MAAM,CAAC,CAAC,CAAC;AACX,KAAK,oBAAoB,GAAG,OAAO,CAAC,yBAAyB,CAAC,qBAAqB,CAAC,EAAE,KAAK,MAAM,EAAE,CAAC,CAAC;AACrG,MAAM,MAAM,qBAAqB,GAAG,OAAO,CAAC,IAAI,CAAC,qBAAqB,EAAE,oBAAoB,CAAC,CAAC,CAAC;AAC/F,MAAM,MAAM,uBAAuB,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAa9D,eAAO,MAAM,8BAA8B,GACzC,UAAS,2BAAgC,KACxC,MAEF,CAAC;AAEF,eAAO,MAAM,8BAA8B,GACzC,UAAS,2BAAgC,KACxC,MAAM,EAER,CAAC;AAEF,eAAO,MAAM,2BAA2B,GACtC,WAAW,eAAe,EAC1B,gBAAe,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAM,KAClD,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAWhC,CAAC;AAEF,qBAIa,uBAAwB,YAAW,SAAS;IAarD,OAAO,CAAC,QAAQ,CAAC,UAAU;IAC3B,OAAO,CAAC,QAAQ,CAAC,QAAQ;IAblB,cAAc,EAAE,iBAAiB,GAAG,EAAE,GAAG,SAAS,CAAC;IACnD,qBAAqB,EAAE,iBAAiB,CAAU;IAClD,mBAAmB,EAAE,eAAe,CAAa;IACjD,oBAAoB,EAAE,gBAAgB,CAAY;IAClD,uBAAuB,UAAS;IAChC,uBAAuB,EAAE,MAAM,GAAG,SAAS,CAAC;IAC5C,sBAAsB,EAAE,eAAe,GAAG,SAAS,CAAC;IAE7D,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAqB;IACpD,OAAO,CAAC,QAAQ,CAAC,sBAAsB,CAAqB;gBAGzC,UAAU,EAAE,UAAU,CAAC,WAAW,CAAC,EACnC,QAAQ,EAAE,SAAS;IAGtC,WAAW,CAAC,QAAQ,EAAE,aAAa,GAAG,IAAI;IAK1C,OAAO,CAAC,WAAW;IA2BnB,OAAO,CAAC,kBAAkB;yCAjDf,uBAAuB;2CAAvB,uBAAuB;CAoEnC;AAED,qBAIa,8BAA+B,YAAW,SAAS;IACrD,qBAAqB,EAAE,qBAAqB,GAAG,IAAI,GAAG,SAAS,CAAC;IAEzE,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAqB;IACnD,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAiE;IAE9F,WAAW,IAAI,IAAI;yCANR,8BAA8B;2CAA9B,8BAA8B;CAmC1C;AAED,qBAIa,gCAAiC,YAAW,SAAS;IAM9D,OAAO,CAAC,QAAQ,CAAC,UAAU;IAC3B,OAAO,CAAC,QAAQ,CAAC,QAAQ;IANlB,uBAAuB,EAAE,uBAAuB,GAAG,IAAI,GAAG,SAAS,CAAC;IAE7E,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAsC;gBAGpD,UAAU,EAAE,UAAU,CAAC,WAAW,CAAC,EACnC,QAAQ,EAAE,SAAS;IAGtC,WAAW,IAAI,IAAI;IAkDnB,OAAO,CAAC,sBAAsB;IAY9B,OAAO,CAAC,oBAAoB;yCAxEjB,gCAAgC;2CAAhC,gCAAgC;CA2E5C;AAED,qBAmIa,uBAAuB;IACrB,OAAO,EAAE,qBAAqB,CAAU;IAC5C,OAAO,EAAE,iBAAiB,CAAU;IACpC,KAAK,EAAE,eAAe,CAAa;IACnC,MAAM,EAAE,gBAAgB,CAAY;IACpC,SAAS,UAAS;IAClB,SAAS,EAAE,MAAM,GAAG,SAAS,CAAC;IAC9B,QAAQ,EAAE,eAAe,GAAG,SAAS,CAAC;IACtC,OAAO,EAAE,qBAAqB,GAAG,SAAS,CAAC;IAC3C,SAAS,EAAE,uBAAuB,GAAG,SAAS,CAAC;IAC/C,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,GAAG,SAAS,CAAC;IAE1D,QAAQ,CAAC,SAAS,2BAAkC;IAE9D,WAAW,CAAC,KAAK,EAAE,UAAU,GAAG,IAAI;yCAdzB,uBAAuB;2CAAvB,uBAAuB;CAiBnC;AAED,qBAgBa,sBAAsB;yCAAtB,sBAAsB;0CAAtB,sBAAsB,+CAtWtB,uBAAuB,SA0EvB,8BAA8B,SAyC9B,gCAAgC,SAgNhC,uBAAuB,yCAnUvB,uBAAuB,SA0EvB,8BAA8B,SAyC9B,gCAAgC,SAgNhC,uBAAuB;0CAmCvB,sBAAsB;CAAG"}

View File

@@ -0,0 +1,545 @@
import { NgStyle, NgSwitch, NgSwitchCase, NgSwitchDefault } from "@angular/common";
import { ChangeDetectionStrategy, Component, Directive, EventEmitter, inject, Input, NgModule, Output, } from "@angular/core";
import { NzTypographyComponent, NzTypographyModule } from "ng-zorro-antd/typography";
import { tachTypographyClassList, tachTypographyClassName, tachTypographyEllipsisStyle, } from "../core";
import * as i0 from "@angular/core";
import * as i1 from "ng-zorro-antd/typography";
const camelToKebab = (value) => value.replace(/[A-Z]/g, match => `-${match.toLowerCase()}`);
const toCssProperty = (styleKey) => {
if (styleKey.startsWith("Webkit")) {
return `-webkit-${camelToKebab(styleKey.slice(6))}`;
}
return camelToKebab(styleKey);
};
export const tachAngularTypographyClassName = (options = {}) => {
return tachTypographyClassName(options);
};
export const tachAngularTypographyClassList = (options = {}) => {
return tachTypographyClassList(options);
};
export const tachAngularTypographyStyles = (ellipsis, preserveStyle = {}) => {
const ellipsisStyle = tachTypographyEllipsisStyle(ellipsis);
if (!ellipsisStyle) {
return preserveStyle;
}
return {
...ellipsisStyle,
...preserveStyle,
};
};
export class TachTypographyDirective {
elementRef;
renderer;
tachTypography;
tachTypographyVariant = "Body";
tachTypographyColor = "primary";
tachTypographyWeight = "normal";
tachTypographyClickable = false;
tachTypographyClassName;
tachTypographyEllipsis;
appliedClasses = new Set();
appliedStyleProperties = new Set();
constructor(elementRef, renderer) {
this.elementRef = elementRef;
this.renderer = renderer;
}
ngOnChanges(_changes) {
this.syncClasses();
this.syncEllipsisStyles();
}
syncClasses() {
const nextClassList = tachTypographyClassList({
variant: this.tachTypography || this.tachTypographyVariant,
color: this.tachTypographyColor,
weight: this.tachTypographyWeight,
clickable: this.tachTypographyClickable,
className: this.tachTypographyClassName,
});
const nextSet = new Set(nextClassList);
for (const className of this.appliedClasses) {
if (!nextSet.has(className)) {
this.renderer.removeClass(this.elementRef.nativeElement, className);
}
}
for (const className of nextSet) {
this.renderer.addClass(this.elementRef.nativeElement, className);
}
this.appliedClasses.clear();
for (const className of nextSet) {
this.appliedClasses.add(className);
}
}
syncEllipsisStyles() {
const nextStyles = tachTypographyEllipsisStyle(this.tachTypographyEllipsis) || {};
const nextStyleKeys = new Set(Object.keys(nextStyles));
for (const styleKey of this.appliedStyleProperties) {
if (!nextStyleKeys.has(styleKey)) {
this.renderer.removeStyle(this.elementRef.nativeElement, toCssProperty(styleKey));
}
}
for (const [styleKey, styleValue] of Object.entries(nextStyles)) {
this.renderer.setStyle(this.elementRef.nativeElement, toCssProperty(styleKey), styleValue);
}
this.appliedStyleProperties.clear();
for (const styleKey of nextStyleKeys) {
this.appliedStyleProperties.add(styleKey);
}
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: TachTypographyDirective, deps: [{ token: i0.ElementRef }, { token: i0.Renderer2 }], target: i0.ɵɵFactoryTarget.Directive });
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "20.3.18", type: TachTypographyDirective, isStandalone: true, selector: "[tachTypography]", inputs: { tachTypography: "tachTypography", tachTypographyVariant: "tachTypographyVariant", tachTypographyColor: "tachTypographyColor", tachTypographyWeight: "tachTypographyWeight", tachTypographyClickable: "tachTypographyClickable", tachTypographyClassName: "tachTypographyClassName", tachTypographyEllipsis: "tachTypographyEllipsis" }, usesOnChanges: true, ngImport: i0 });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: TachTypographyDirective, decorators: [{
type: Directive,
args: [{
selector: "[tachTypography]",
standalone: true,
}]
}], ctorParameters: () => [{ type: i0.ElementRef }, { type: i0.Renderer2 }], propDecorators: { tachTypography: [{
type: Input
}], tachTypographyVariant: [{
type: Input
}], tachTypographyColor: [{
type: Input
}], tachTypographyWeight: [{
type: Input
}], tachTypographyClickable: [{
type: Input
}], tachTypographyClassName: [{
type: Input
}], tachTypographyEllipsis: [{
type: Input
}] } });
export class TachTypographyNzPropsDirective {
tachTypographyNzProps;
appliedNzKeys = new Set();
nzTypography = inject(NzTypographyComponent, { self: true, optional: true });
ngOnChanges() {
if (!this.nzTypography) {
return;
}
const nzTypography = this.nzTypography;
const nextProps = this.tachTypographyNzProps ?? {};
const nextKeys = new Set();
for (const [key, value] of Object.entries(nextProps)) {
if (!key.startsWith("nz")) {
continue;
}
nzTypography[key] = value;
nextKeys.add(key);
}
for (const key of this.appliedNzKeys) {
if (!nextKeys.has(key)) {
nzTypography[key] = undefined;
}
}
this.appliedNzKeys.clear();
for (const key of nextKeys) {
this.appliedNzKeys.add(key);
}
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: TachTypographyNzPropsDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "20.3.18", type: TachTypographyNzPropsDirective, isStandalone: true, selector: "[tachTypographyNzProps]", inputs: { tachTypographyNzProps: "tachTypographyNzProps" }, usesOnChanges: true, ngImport: i0 });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: TachTypographyNzPropsDirective, decorators: [{
type: Directive,
args: [{
selector: "[tachTypographyNzProps]",
standalone: true,
}]
}], propDecorators: { tachTypographyNzProps: [{
type: Input
}] } });
export class TachTypographyHostPropsDirective {
elementRef;
renderer;
tachTypographyHostProps;
appliedHostProps = new Map();
constructor(elementRef, renderer) {
this.elementRef = elementRef;
this.renderer = renderer;
}
ngOnChanges() {
const nextProps = this.tachTypographyHostProps ?? {};
const nextAppliedProps = new Map();
for (const [key, value] of Object.entries(nextProps)) {
if (key === "class" || key === "className" || key === "style") {
continue;
}
if (value === undefined || value === null) {
continue;
}
const applyAsAttr = this.shouldApplyAsAttribute(key, value);
if (applyAsAttr) {
if (typeof value === "boolean") {
if (value) {
this.renderer.setAttribute(this.elementRef.nativeElement, key, "");
}
else {
this.renderer.removeAttribute(this.elementRef.nativeElement, key);
}
}
else {
this.renderer.setAttribute(this.elementRef.nativeElement, key, String(value));
}
nextAppliedProps.set(key, "attr");
continue;
}
this.renderer.setProperty(this.elementRef.nativeElement, key, value);
nextAppliedProps.set(key, "prop");
}
for (const [key, kind] of this.appliedHostProps.entries()) {
if (nextAppliedProps.has(key)) {
continue;
}
if (kind === "attr") {
this.renderer.removeAttribute(this.elementRef.nativeElement, key);
}
else {
this.renderer.setProperty(this.elementRef.nativeElement, key, undefined);
}
}
this.appliedHostProps.clear();
for (const [key, kind] of nextAppliedProps.entries()) {
this.appliedHostProps.set(key, kind);
}
}
shouldApplyAsAttribute(key, value) {
if (key.startsWith("data-") || key.startsWith("aria-")) {
return true;
}
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
return !this.hasPropertyOnElement(key);
}
return false;
}
hasPropertyOnElement(key) {
return key in this.elementRef.nativeElement;
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: TachTypographyHostPropsDirective, deps: [{ token: i0.ElementRef }, { token: i0.Renderer2 }], target: i0.ɵɵFactoryTarget.Directive });
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "20.3.18", type: TachTypographyHostPropsDirective, isStandalone: true, selector: "[tachTypographyHostProps]", inputs: { tachTypographyHostProps: "tachTypographyHostProps" }, usesOnChanges: true, ngImport: i0 });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: TachTypographyHostPropsDirective, decorators: [{
type: Directive,
args: [{
selector: "[tachTypographyHostProps]",
standalone: true,
}]
}], ctorParameters: () => [{ type: i0.ElementRef }, { type: i0.Renderer2 }], propDecorators: { tachTypographyHostProps: [{
type: Input
}] } });
export class TachTypographyComponent {
hostTag = "span";
variant = "Body";
color = "primary";
weight = "normal";
clickable = false;
className;
ellipsis;
nzProps;
hostProps;
preserveStyle;
tachClick = new EventEmitter();
handleClick(event) {
this.tachClick.emit(event);
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: TachTypographyComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.18", type: TachTypographyComponent, isStandalone: true, selector: "tach-typography", inputs: { hostTag: ["as", "hostTag"], variant: "variant", color: "color", weight: "weight", clickable: "clickable", className: "className", ellipsis: "ellipsis", nzProps: "nzProps", hostProps: "hostProps", preserveStyle: "preserveStyle" }, outputs: { tachClick: "tachClick" }, ngImport: i0, template: `
<ng-container [ngSwitch]="hostTag">
<p
*ngSwitchCase="'p'"
nz-typography
[tachTypography]="variant"
[tachTypographyColor]="color"
[tachTypographyWeight]="weight"
[tachTypographyClickable]="clickable"
[tachTypographyClassName]="className"
[tachTypographyEllipsis]="ellipsis"
[tachTypographyNzProps]="nzProps"
[tachTypographyHostProps]="hostProps"
[ngStyle]="preserveStyle"
(click)="handleClick($event)"
>
<ng-content />
</p>
<a
*ngSwitchCase="'a'"
nz-typography
[tachTypography]="variant"
[tachTypographyColor]="color"
[tachTypographyWeight]="weight"
[tachTypographyClickable]="clickable"
[tachTypographyClassName]="className"
[tachTypographyEllipsis]="ellipsis"
[tachTypographyNzProps]="nzProps"
[tachTypographyHostProps]="hostProps"
[ngStyle]="preserveStyle"
(click)="handleClick($event)"
>
<ng-content />
</a>
<h1
*ngSwitchCase="'h1'"
nz-typography
[tachTypography]="variant"
[tachTypographyColor]="color"
[tachTypographyWeight]="weight"
[tachTypographyClickable]="clickable"
[tachTypographyClassName]="className"
[tachTypographyEllipsis]="ellipsis"
[tachTypographyNzProps]="nzProps"
[tachTypographyHostProps]="hostProps"
[ngStyle]="preserveStyle"
(click)="handleClick($event)"
>
<ng-content />
</h1>
<h2
*ngSwitchCase="'h2'"
nz-typography
[tachTypography]="variant"
[tachTypographyColor]="color"
[tachTypographyWeight]="weight"
[tachTypographyClickable]="clickable"
[tachTypographyClassName]="className"
[tachTypographyEllipsis]="ellipsis"
[tachTypographyNzProps]="nzProps"
[tachTypographyHostProps]="hostProps"
[ngStyle]="preserveStyle"
(click)="handleClick($event)"
>
<ng-content />
</h2>
<h3
*ngSwitchCase="'h3'"
nz-typography
[tachTypography]="variant"
[tachTypographyColor]="color"
[tachTypographyWeight]="weight"
[tachTypographyClickable]="clickable"
[tachTypographyClassName]="className"
[tachTypographyEllipsis]="ellipsis"
[tachTypographyNzProps]="nzProps"
[tachTypographyHostProps]="hostProps"
[ngStyle]="preserveStyle"
(click)="handleClick($event)"
>
<ng-content />
</h3>
<h4
*ngSwitchCase="'h4'"
nz-typography
[tachTypography]="variant"
[tachTypographyColor]="color"
[tachTypographyWeight]="weight"
[tachTypographyClickable]="clickable"
[tachTypographyClassName]="className"
[tachTypographyEllipsis]="ellipsis"
[tachTypographyNzProps]="nzProps"
[tachTypographyHostProps]="hostProps"
[ngStyle]="preserveStyle"
(click)="handleClick($event)"
>
<ng-content />
</h4>
<span
*ngSwitchDefault
nz-typography
[tachTypography]="variant"
[tachTypographyColor]="color"
[tachTypographyWeight]="weight"
[tachTypographyClickable]="clickable"
[tachTypographyClassName]="className"
[tachTypographyEllipsis]="ellipsis"
[tachTypographyNzProps]="nzProps"
[tachTypographyHostProps]="hostProps"
[ngStyle]="preserveStyle"
(click)="handleClick($event)"
>
<ng-content />
</span>
</ng-container>
`, isInline: true, dependencies: [{ kind: "ngmodule", type: NzTypographyModule }, { kind: "component", type: i1.NzTypographyComponent, selector: " nz-typography, [nz-typography], p[nz-paragraph], span[nz-text], h1[nz-title], h2[nz-title], h3[nz-title], h4[nz-title] ", inputs: ["nzCopyable", "nzEditable", "nzDisabled", "nzExpandable", "nzEllipsis", "nzCopyTooltips", "nzCopyIcons", "nzEditTooltip", "nzEditIcon", "nzContent", "nzEllipsisRows", "nzType", "nzCopyText", "nzSuffix"], outputs: ["nzContentChange", "nzCopy", "nzExpandChange", "nzOnEllipsis"], exportAs: ["nzTypography"] }, { kind: "directive", type: TachTypographyDirective, selector: "[tachTypography]", inputs: ["tachTypography", "tachTypographyVariant", "tachTypographyColor", "tachTypographyWeight", "tachTypographyClickable", "tachTypographyClassName", "tachTypographyEllipsis"] }, { kind: "directive", type: TachTypographyNzPropsDirective, selector: "[tachTypographyNzProps]", inputs: ["tachTypographyNzProps"] }, { kind: "directive", type: TachTypographyHostPropsDirective, selector: "[tachTypographyHostProps]", inputs: ["tachTypographyHostProps"] }, { kind: "directive", type: NgSwitch, selector: "[ngSwitch]", inputs: ["ngSwitch"] }, { kind: "directive", type: NgSwitchCase, selector: "[ngSwitchCase]", inputs: ["ngSwitchCase"] }, { kind: "directive", type: NgSwitchDefault, selector: "[ngSwitchDefault]" }, { kind: "directive", type: NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: TachTypographyComponent, decorators: [{
type: Component,
args: [{
selector: "tach-typography",
standalone: true,
imports: [
NzTypographyModule,
TachTypographyDirective,
TachTypographyNzPropsDirective,
TachTypographyHostPropsDirective,
NgSwitch,
NgSwitchCase,
NgSwitchDefault,
NgStyle,
],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<ng-container [ngSwitch]="hostTag">
<p
*ngSwitchCase="'p'"
nz-typography
[tachTypography]="variant"
[tachTypographyColor]="color"
[tachTypographyWeight]="weight"
[tachTypographyClickable]="clickable"
[tachTypographyClassName]="className"
[tachTypographyEllipsis]="ellipsis"
[tachTypographyNzProps]="nzProps"
[tachTypographyHostProps]="hostProps"
[ngStyle]="preserveStyle"
(click)="handleClick($event)"
>
<ng-content />
</p>
<a
*ngSwitchCase="'a'"
nz-typography
[tachTypography]="variant"
[tachTypographyColor]="color"
[tachTypographyWeight]="weight"
[tachTypographyClickable]="clickable"
[tachTypographyClassName]="className"
[tachTypographyEllipsis]="ellipsis"
[tachTypographyNzProps]="nzProps"
[tachTypographyHostProps]="hostProps"
[ngStyle]="preserveStyle"
(click)="handleClick($event)"
>
<ng-content />
</a>
<h1
*ngSwitchCase="'h1'"
nz-typography
[tachTypography]="variant"
[tachTypographyColor]="color"
[tachTypographyWeight]="weight"
[tachTypographyClickable]="clickable"
[tachTypographyClassName]="className"
[tachTypographyEllipsis]="ellipsis"
[tachTypographyNzProps]="nzProps"
[tachTypographyHostProps]="hostProps"
[ngStyle]="preserveStyle"
(click)="handleClick($event)"
>
<ng-content />
</h1>
<h2
*ngSwitchCase="'h2'"
nz-typography
[tachTypography]="variant"
[tachTypographyColor]="color"
[tachTypographyWeight]="weight"
[tachTypographyClickable]="clickable"
[tachTypographyClassName]="className"
[tachTypographyEllipsis]="ellipsis"
[tachTypographyNzProps]="nzProps"
[tachTypographyHostProps]="hostProps"
[ngStyle]="preserveStyle"
(click)="handleClick($event)"
>
<ng-content />
</h2>
<h3
*ngSwitchCase="'h3'"
nz-typography
[tachTypography]="variant"
[tachTypographyColor]="color"
[tachTypographyWeight]="weight"
[tachTypographyClickable]="clickable"
[tachTypographyClassName]="className"
[tachTypographyEllipsis]="ellipsis"
[tachTypographyNzProps]="nzProps"
[tachTypographyHostProps]="hostProps"
[ngStyle]="preserveStyle"
(click)="handleClick($event)"
>
<ng-content />
</h3>
<h4
*ngSwitchCase="'h4'"
nz-typography
[tachTypography]="variant"
[tachTypographyColor]="color"
[tachTypographyWeight]="weight"
[tachTypographyClickable]="clickable"
[tachTypographyClassName]="className"
[tachTypographyEllipsis]="ellipsis"
[tachTypographyNzProps]="nzProps"
[tachTypographyHostProps]="hostProps"
[ngStyle]="preserveStyle"
(click)="handleClick($event)"
>
<ng-content />
</h4>
<span
*ngSwitchDefault
nz-typography
[tachTypography]="variant"
[tachTypographyColor]="color"
[tachTypographyWeight]="weight"
[tachTypographyClickable]="clickable"
[tachTypographyClassName]="className"
[tachTypographyEllipsis]="ellipsis"
[tachTypographyNzProps]="nzProps"
[tachTypographyHostProps]="hostProps"
[ngStyle]="preserveStyle"
(click)="handleClick($event)"
>
<ng-content />
</span>
</ng-container>
`,
}]
}], propDecorators: { hostTag: [{
type: Input,
args: ["as"]
}], variant: [{
type: Input
}], color: [{
type: Input
}], weight: [{
type: Input
}], clickable: [{
type: Input
}], className: [{
type: Input
}], ellipsis: [{
type: Input
}], nzProps: [{
type: Input
}], hostProps: [{
type: Input
}], preserveStyle: [{
type: Input
}], tachClick: [{
type: Output
}] } });
export class TachTypographyNzModule {
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: TachTypographyNzModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule });
static ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "20.3.18", ngImport: i0, type: TachTypographyNzModule, imports: [NzTypographyModule, TachTypographyDirective, TachTypographyNzPropsDirective, TachTypographyHostPropsDirective, TachTypographyComponent], exports: [NzTypographyModule, TachTypographyDirective, TachTypographyNzPropsDirective, TachTypographyHostPropsDirective, TachTypographyComponent] });
static ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: TachTypographyNzModule, imports: [NzTypographyModule,
TachTypographyComponent, NzTypographyModule] });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: TachTypographyNzModule, decorators: [{
type: NgModule,
args: [{
imports: [
NzTypographyModule,
TachTypographyDirective,
TachTypographyNzPropsDirective,
TachTypographyHostPropsDirective,
TachTypographyComponent,
],
exports: [
NzTypographyModule,
TachTypographyDirective,
TachTypographyNzPropsDirective,
TachTypographyHostPropsDirective,
TachTypographyComponent,
],
}]
}] });
//# sourceMappingURL=index.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,2 @@
export * from "./index";
//# sourceMappingURL=public-api.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"public-api.d.ts","sourceRoot":"","sources":["../../../src/angular/public-api.ts"],"names":[],"mappings":"AAAA,cAAc,SAAS,CAAC"}

View File

@@ -0,0 +1,2 @@
export * from "./index";
//# sourceMappingURL=public-api.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"public-api.js","sourceRoot":"","sources":["../../../src/angular/public-api.ts"],"names":[],"mappings":"AAAA,cAAc,SAAS,CAAC"}

View File

@@ -0,0 +1,4 @@
import type { TypographyClassOptions } from "./types";
export declare const tachTypographyClassName: ({ variant, color, weight, clickable, className, }?: TypographyClassOptions) => string;
export declare const tachTypographyClassList: (options?: TypographyClassOptions) => string[];
//# sourceMappingURL=classnames.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"classnames.d.ts","sourceRoot":"","sources":["../../../src/core/classnames.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,SAAS,CAAC;AAOtD,eAAO,MAAM,uBAAuB,GAAI,oDAMrC,sBAA2B,KAAG,MAShC,CAAC;AAEF,eAAO,MAAM,uBAAuB,GAAI,UAAS,sBAA2B,KAAG,MAAM,EAIpF,CAAC"}

View File

@@ -0,0 +1,11 @@
const BASE_CLASS = "tach-typography";
const join = (...parts) => parts.filter(Boolean).join(" ");
export const tachTypographyClassName = ({ variant = "Body", color = "primary", weight = "normal", clickable = false, className, } = {}) => {
return join(BASE_CLASS, `${BASE_CLASS}--${variant}`, `${BASE_CLASS}--color-${color}`, weight === "bold" && `${BASE_CLASS}--bold`, clickable && `${BASE_CLASS}--pointer`, className);
};
export const tachTypographyClassList = (options = {}) => {
return tachTypographyClassName(options)
.split(" ")
.filter(Boolean);
};
//# sourceMappingURL=classnames.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"classnames.js","sourceRoot":"","sources":["../../../src/core/classnames.ts"],"names":[],"mappings":"AAEA,MAAM,UAAU,GAAG,iBAAiB,CAAC;AAErC,MAAM,IAAI,GAAG,CAAC,GAAG,KAA+C,EAAU,EAAE,CAC1E,KAAK,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AAElC,MAAM,CAAC,MAAM,uBAAuB,GAAG,CAAC,EACtC,OAAO,GAAG,MAAM,EAChB,KAAK,GAAG,SAAS,EACjB,MAAM,GAAG,QAAQ,EACjB,SAAS,GAAG,KAAK,EACjB,SAAS,MACiB,EAAE,EAAU,EAAE;IACxC,OAAO,IAAI,CACT,UAAU,EACV,GAAG,UAAU,KAAK,OAAO,EAAE,EAC3B,GAAG,UAAU,WAAW,KAAK,EAAE,EAC/B,MAAM,KAAK,MAAM,IAAI,GAAG,UAAU,QAAQ,EAC1C,SAAS,IAAI,GAAG,UAAU,WAAW,EACrC,SAAS,CACV,CAAC;AACJ,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,uBAAuB,GAAG,CAAC,UAAkC,EAAE,EAAY,EAAE;IACxF,OAAO,uBAAuB,CAAC,OAAO,CAAC;SACpC,KAAK,CAAC,GAAG,CAAC;SACV,MAAM,CAAC,OAAO,CAAC,CAAC;AACrB,CAAC,CAAC"}

View File

@@ -0,0 +1,5 @@
import type { EllipsisOptions } from "./types";
type StyleRecord = Record<string, string | number>;
export declare const tachTypographyEllipsisStyle: (ellipsis?: EllipsisOptions) => StyleRecord | undefined;
export {};
//# sourceMappingURL=ellipsis.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"ellipsis.d.ts","sourceRoot":"","sources":["../../../src/core/ellipsis.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAE/C,KAAK,WAAW,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,CAAC;AAEnD,eAAO,MAAM,2BAA2B,GACtC,WAAW,eAAe,KACzB,WAAW,GAAG,SAchB,CAAC"}

View File

@@ -0,0 +1,14 @@
export const tachTypographyEllipsisStyle = (ellipsis) => {
if (!ellipsis) {
return undefined;
}
const rows = typeof ellipsis === "object" ? ellipsis.rows ?? 1 : 1;
return {
overflow: "hidden",
textOverflow: "ellipsis",
display: "-webkit-box",
WebkitBoxOrient: "vertical",
WebkitLineClamp: rows,
};
};
//# sourceMappingURL=ellipsis.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"ellipsis.js","sourceRoot":"","sources":["../../../src/core/ellipsis.ts"],"names":[],"mappings":"AAIA,MAAM,CAAC,MAAM,2BAA2B,GAAG,CACzC,QAA0B,EACD,EAAE;IAC3B,IAAI,CAAC,QAAQ,EAAE,CAAC;QACd,OAAO,SAAS,CAAC;IACnB,CAAC;IAED,MAAM,IAAI,GAAG,OAAO,QAAQ,KAAK,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,IAAI,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAEnE,OAAO;QACL,QAAQ,EAAE,QAAQ;QAClB,YAAY,EAAE,UAAU;QACxB,OAAO,EAAE,aAAa;QACtB,eAAe,EAAE,UAAU;QAC3B,eAAe,EAAE,IAAI;KACtB,CAAC;AACJ,CAAC,CAAC"}

View File

@@ -0,0 +1,4 @@
export * from "./types";
export * from "./classnames";
export * from "./ellipsis";
//# sourceMappingURL=index.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/core/index.ts"],"names":[],"mappings":"AAAA,cAAc,SAAS,CAAC;AACxB,cAAc,cAAc,CAAC;AAC7B,cAAc,YAAY,CAAC"}

View File

@@ -0,0 +1,4 @@
export * from "./types";
export * from "./classnames";
export * from "./ellipsis";
//# sourceMappingURL=index.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/core/index.ts"],"names":[],"mappings":"AAAA,cAAc,SAAS,CAAC;AACxB,cAAc,cAAc,CAAC;AAC7B,cAAc,YAAY,CAAC"}

View File

@@ -0,0 +1,19 @@
export declare const TYPOGRAPHY_VARIANTS: readonly ["LargeTitle", "Title1", "Title2", "Title3", "Headline", "Body", "Inputs", "Subheadline", "FootnoteUnderline", "Footnote", "Caption", "Caption2", "AccentH1", "AccentH2", "AccentSubttl", "AccentSubttl2", "AccentCaption", "AccentCaption2", "AccentRegularM", "AccentRegularS", "AccentLargeTtl", "AppMediumBody", "AppMediumSubtext", "AppMediumSubtextUnderline"];
export declare const TYPOGRAPHY_COLORS: readonly ["primary", "secondary", "tertiary", "quaternary", "link", "white", "dark", "alert", "malahit", "attantion"];
export type TypographyVariant = (typeof TYPOGRAPHY_VARIANTS)[number];
export type TypographyColor = (typeof TYPOGRAPHY_COLORS)[number];
export type TypographyWeight = "normal" | "bold";
export interface TypographyClassOptions {
variant?: TypographyVariant;
color?: TypographyColor;
weight?: TypographyWeight;
clickable?: boolean;
className?: string | undefined;
}
export type EllipsisOptions = boolean | {
rows?: number;
};
export interface TypographyRenderOptions extends TypographyClassOptions {
ellipsis?: EllipsisOptions;
}
//# sourceMappingURL=types.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/core/types.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,mBAAmB,gXAyBtB,CAAC;AAEX,eAAO,MAAM,iBAAiB,uHAWpB,CAAC;AAEX,MAAM,MAAM,iBAAiB,GAAG,CAAC,OAAO,mBAAmB,CAAC,CAAC,MAAM,CAAC,CAAC;AACrE,MAAM,MAAM,eAAe,GAAG,CAAC,OAAO,iBAAiB,CAAC,CAAC,MAAM,CAAC,CAAC;AACjE,MAAM,MAAM,gBAAgB,GAAG,QAAQ,GAAG,MAAM,CAAC;AAEjD,MAAM,WAAW,sBAAsB;IACrC,OAAO,CAAC,EAAE,iBAAiB,CAAC;IAC5B,KAAK,CAAC,EAAE,eAAe,CAAC;IACxB,MAAM,CAAC,EAAE,gBAAgB,CAAC;IAC1B,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAChC;AAED,MAAM,MAAM,eAAe,GACvB,OAAO,GACP;IACE,IAAI,CAAC,EAAE,MAAM,CAAC;CACf,CAAC;AAEN,MAAM,WAAW,uBAAwB,SAAQ,sBAAsB;IACrE,QAAQ,CAAC,EAAE,eAAe,CAAC;CAC5B"}

View File

@@ -0,0 +1,39 @@
export const TYPOGRAPHY_VARIANTS = [
"LargeTitle",
"Title1",
"Title2",
"Title3",
"Headline",
"Body",
"Inputs",
"Subheadline",
"FootnoteUnderline",
"Footnote",
"Caption",
"Caption2",
"AccentH1",
"AccentH2",
"AccentSubttl",
"AccentSubttl2",
"AccentCaption",
"AccentCaption2",
"AccentRegularM",
"AccentRegularS",
"AccentLargeTtl",
"AppMediumBody",
"AppMediumSubtext",
"AppMediumSubtextUnderline",
];
export const TYPOGRAPHY_COLORS = [
"primary",
"secondary",
"tertiary",
"quaternary",
"link",
"white",
"dark",
"alert",
"malahit",
"attantion",
];
//# sourceMappingURL=types.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../../src/core/types.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,mBAAmB,GAAG;IACjC,YAAY;IACZ,QAAQ;IACR,QAAQ;IACR,QAAQ;IACR,UAAU;IACV,MAAM;IACN,QAAQ;IACR,aAAa;IACb,mBAAmB;IACnB,UAAU;IACV,SAAS;IACT,UAAU;IACV,UAAU;IACV,UAAU;IACV,cAAc;IACd,eAAe;IACf,eAAe;IACf,gBAAgB;IAChB,gBAAgB;IAChB,gBAAgB;IAChB,gBAAgB;IAChB,eAAe;IACf,kBAAkB;IAClB,2BAA2B;CACnB,CAAC;AAEX,MAAM,CAAC,MAAM,iBAAiB,GAAG;IAC/B,SAAS;IACT,WAAW;IACX,UAAU;IACV,YAAY;IACZ,MAAM;IACN,OAAO;IACP,MAAM;IACN,OAAO;IACP,SAAS;IACT,WAAW;CACH,CAAC"}

View File

@@ -0,0 +1 @@
export * from "./angular/index";

View File

@@ -0,0 +1 @@
export * from "./angular/index.js";

View File

@@ -0,0 +1,87 @@
'use strict';
// src/core/types.ts
var TYPOGRAPHY_VARIANTS = [
"LargeTitle",
"Title1",
"Title2",
"Title3",
"Headline",
"Body",
"Inputs",
"Subheadline",
"FootnoteUnderline",
"Footnote",
"Caption",
"Caption2",
"AccentH1",
"AccentH2",
"AccentSubttl",
"AccentSubttl2",
"AccentCaption",
"AccentCaption2",
"AccentRegularM",
"AccentRegularS",
"AccentLargeTtl",
"AppMediumBody",
"AppMediumSubtext",
"AppMediumSubtextUnderline"
];
var TYPOGRAPHY_COLORS = [
"primary",
"secondary",
"tertiary",
"quaternary",
"link",
"white",
"dark",
"alert",
"malahit",
"attantion"
];
// src/core/classnames.ts
var BASE_CLASS = "tach-typography";
var join = (...parts) => parts.filter(Boolean).join(" ");
var tachTypographyClassName = ({
variant = "Body",
color = "primary",
weight = "normal",
clickable = false,
className
} = {}) => {
return join(
BASE_CLASS,
`${BASE_CLASS}--${variant}`,
`${BASE_CLASS}--color-${color}`,
weight === "bold" && `${BASE_CLASS}--bold`,
clickable && `${BASE_CLASS}--pointer`,
className
);
};
var tachTypographyClassList = (options = {}) => {
return tachTypographyClassName(options).split(" ").filter(Boolean);
};
// src/core/ellipsis.ts
var tachTypographyEllipsisStyle = (ellipsis) => {
if (!ellipsis) {
return void 0;
}
const rows = typeof ellipsis === "object" ? ellipsis.rows ?? 1 : 1;
return {
overflow: "hidden",
textOverflow: "ellipsis",
display: "-webkit-box",
WebkitBoxOrient: "vertical",
WebkitLineClamp: rows
};
};
exports.TYPOGRAPHY_COLORS = TYPOGRAPHY_COLORS;
exports.TYPOGRAPHY_VARIANTS = TYPOGRAPHY_VARIANTS;
exports.tachTypographyClassList = tachTypographyClassList;
exports.tachTypographyClassName = tachTypographyClassName;
exports.tachTypographyEllipsisStyle = tachTypographyEllipsisStyle;
//# sourceMappingURL=index.cjs.map
//# sourceMappingURL=index.cjs.map

View File

@@ -0,0 +1 @@
{"version":3,"sources":["../../src/core/types.ts","../../src/core/classnames.ts","../../src/core/ellipsis.ts"],"names":[],"mappings":";;;AAAO,IAAM,mBAAA,GAAsB;AAAA,EACjC,YAAA;AAAA,EACA,QAAA;AAAA,EACA,QAAA;AAAA,EACA,QAAA;AAAA,EACA,UAAA;AAAA,EACA,MAAA;AAAA,EACA,QAAA;AAAA,EACA,aAAA;AAAA,EACA,mBAAA;AAAA,EACA,UAAA;AAAA,EACA,SAAA;AAAA,EACA,UAAA;AAAA,EACA,UAAA;AAAA,EACA,UAAA;AAAA,EACA,cAAA;AAAA,EACA,eAAA;AAAA,EACA,eAAA;AAAA,EACA,gBAAA;AAAA,EACA,gBAAA;AAAA,EACA,gBAAA;AAAA,EACA,gBAAA;AAAA,EACA,eAAA;AAAA,EACA,kBAAA;AAAA,EACA;AACF;AAEO,IAAM,iBAAA,GAAoB;AAAA,EAC/B,SAAA;AAAA,EACA,WAAA;AAAA,EACA,UAAA;AAAA,EACA,YAAA;AAAA,EACA,MAAA;AAAA,EACA,OAAA;AAAA,EACA,MAAA;AAAA,EACA,OAAA;AAAA,EACA,SAAA;AAAA,EACA;AACF;;;ACpCA,IAAM,UAAA,GAAa,iBAAA;AAEnB,IAAM,IAAA,GAAO,IAAI,KAAA,KACf,KAAA,CAAM,OAAO,OAAO,CAAA,CAAE,KAAK,GAAG,CAAA;AAEzB,IAAM,0BAA0B,CAAC;AAAA,EACtC,OAAA,GAAU,MAAA;AAAA,EACV,KAAA,GAAQ,SAAA;AAAA,EACR,MAAA,GAAS,QAAA;AAAA,EACT,SAAA,GAAY,KAAA;AAAA,EACZ;AACF,CAAA,GAA4B,EAAC,KAAc;AACzC,EAAA,OAAO,IAAA;AAAA,IACL,UAAA;AAAA,IACA,CAAA,EAAG,UAAU,CAAA,EAAA,EAAK,OAAO,CAAA,CAAA;AAAA,IACzB,CAAA,EAAG,UAAU,CAAA,QAAA,EAAW,KAAK,CAAA,CAAA;AAAA,IAC7B,MAAA,KAAW,MAAA,IAAU,CAAA,EAAG,UAAU,CAAA,MAAA,CAAA;AAAA,IAClC,SAAA,IAAa,GAAG,UAAU,CAAA,SAAA,CAAA;AAAA,IAC1B;AAAA,GACF;AACF;AAEO,IAAM,uBAAA,GAA0B,CAAC,OAAA,GAAkC,EAAC,KAAgB;AACzF,EAAA,OAAO,wBAAwB,OAAO,CAAA,CACnC,MAAM,GAAG,CAAA,CACT,OAAO,OAAO,CAAA;AACnB;;;ACxBO,IAAM,2BAAA,GAA8B,CACzC,QAAA,KAC4B;AAC5B,EAAA,IAAI,CAAC,QAAA,EAAU;AACb,IAAA,OAAO,MAAA;AAAA,EACT;AAEA,EAAA,MAAM,OAAO,OAAO,QAAA,KAAa,QAAA,GAAW,QAAA,CAAS,QAAQ,CAAA,GAAI,CAAA;AAEjE,EAAA,OAAO;AAAA,IACL,QAAA,EAAU,QAAA;AAAA,IACV,YAAA,EAAc,UAAA;AAAA,IACd,OAAA,EAAS,aAAA;AAAA,IACT,eAAA,EAAiB,UAAA;AAAA,IACjB,eAAA,EAAiB;AAAA,GACnB;AACF","file":"index.cjs","sourcesContent":["export const TYPOGRAPHY_VARIANTS = [\n \"LargeTitle\",\n \"Title1\",\n \"Title2\",\n \"Title3\",\n \"Headline\",\n \"Body\",\n \"Inputs\",\n \"Subheadline\",\n \"FootnoteUnderline\",\n \"Footnote\",\n \"Caption\",\n \"Caption2\",\n \"AccentH1\",\n \"AccentH2\",\n \"AccentSubttl\",\n \"AccentSubttl2\",\n \"AccentCaption\",\n \"AccentCaption2\",\n \"AccentRegularM\",\n \"AccentRegularS\",\n \"AccentLargeTtl\",\n \"AppMediumBody\",\n \"AppMediumSubtext\",\n \"AppMediumSubtextUnderline\",\n] as const;\n\nexport const TYPOGRAPHY_COLORS = [\n \"primary\",\n \"secondary\",\n \"tertiary\",\n \"quaternary\",\n \"link\",\n \"white\",\n \"dark\",\n \"alert\",\n \"malahit\",\n \"attantion\",\n] as const;\n\nexport type TypographyVariant = (typeof TYPOGRAPHY_VARIANTS)[number];\nexport type TypographyColor = (typeof TYPOGRAPHY_COLORS)[number];\nexport type TypographyWeight = \"normal\" | \"bold\";\n\nexport interface TypographyClassOptions {\n variant?: TypographyVariant;\n color?: TypographyColor;\n weight?: TypographyWeight;\n clickable?: boolean;\n className?: string | undefined;\n}\n\nexport type EllipsisOptions =\n | boolean\n | {\n rows?: number;\n };\n\nexport interface TypographyRenderOptions extends TypographyClassOptions {\n ellipsis?: EllipsisOptions;\n}\n","import type { TypographyClassOptions } from \"./types\";\n\nconst BASE_CLASS = \"tach-typography\";\n\nconst join = (...parts: Array<string | undefined | null | false>): string =>\n parts.filter(Boolean).join(\" \");\n\nexport const tachTypographyClassName = ({\n variant = \"Body\",\n color = \"primary\",\n weight = \"normal\",\n clickable = false,\n className,\n}: TypographyClassOptions = {}): string => {\n return join(\n BASE_CLASS,\n `${BASE_CLASS}--${variant}`,\n `${BASE_CLASS}--color-${color}`,\n weight === \"bold\" && `${BASE_CLASS}--bold`,\n clickable && `${BASE_CLASS}--pointer`,\n className,\n );\n};\n\nexport const tachTypographyClassList = (options: TypographyClassOptions = {}): string[] => {\n return tachTypographyClassName(options)\n .split(\" \")\n .filter(Boolean);\n};\n","import type { EllipsisOptions } from \"./types\";\n\ntype StyleRecord = Record<string, string | number>;\n\nexport const tachTypographyEllipsisStyle = (\n ellipsis?: EllipsisOptions,\n): StyleRecord | undefined => {\n if (!ellipsis) {\n return undefined;\n }\n\n const rows = typeof ellipsis === \"object\" ? ellipsis.rows ?? 1 : 1;\n\n return {\n overflow: \"hidden\",\n textOverflow: \"ellipsis\",\n display: \"-webkit-box\",\n WebkitBoxOrient: \"vertical\",\n WebkitLineClamp: rows,\n };\n};\n"]}

View File

@@ -0,0 +1,10 @@
import { T as TypographyClassOptions, E as EllipsisOptions } from '../types-CQyFuLqp.cjs';
export { a as TYPOGRAPHY_COLORS, b as TYPOGRAPHY_VARIANTS, c as TypographyColor, d as TypographyRenderOptions, e as TypographyVariant, f as TypographyWeight } from '../types-CQyFuLqp.cjs';
declare const tachTypographyClassName: ({ variant, color, weight, clickable, className, }?: TypographyClassOptions) => string;
declare const tachTypographyClassList: (options?: TypographyClassOptions) => string[];
type StyleRecord = Record<string, string | number>;
declare const tachTypographyEllipsisStyle: (ellipsis?: EllipsisOptions) => StyleRecord | undefined;
export { EllipsisOptions, TypographyClassOptions, tachTypographyClassList, tachTypographyClassName, tachTypographyEllipsisStyle };

View File

@@ -0,0 +1,10 @@
import { T as TypographyClassOptions, E as EllipsisOptions } from '../types-CQyFuLqp.js';
export { a as TYPOGRAPHY_COLORS, b as TYPOGRAPHY_VARIANTS, c as TypographyColor, d as TypographyRenderOptions, e as TypographyVariant, f as TypographyWeight } from '../types-CQyFuLqp.js';
declare const tachTypographyClassName: ({ variant, color, weight, clickable, className, }?: TypographyClassOptions) => string;
declare const tachTypographyClassList: (options?: TypographyClassOptions) => string[];
type StyleRecord = Record<string, string | number>;
declare const tachTypographyEllipsisStyle: (ellipsis?: EllipsisOptions) => StyleRecord | undefined;
export { EllipsisOptions, TypographyClassOptions, tachTypographyClassList, tachTypographyClassName, tachTypographyEllipsisStyle };

View File

@@ -0,0 +1,81 @@
// src/core/types.ts
var TYPOGRAPHY_VARIANTS = [
"LargeTitle",
"Title1",
"Title2",
"Title3",
"Headline",
"Body",
"Inputs",
"Subheadline",
"FootnoteUnderline",
"Footnote",
"Caption",
"Caption2",
"AccentH1",
"AccentH2",
"AccentSubttl",
"AccentSubttl2",
"AccentCaption",
"AccentCaption2",
"AccentRegularM",
"AccentRegularS",
"AccentLargeTtl",
"AppMediumBody",
"AppMediumSubtext",
"AppMediumSubtextUnderline"
];
var TYPOGRAPHY_COLORS = [
"primary",
"secondary",
"tertiary",
"quaternary",
"link",
"white",
"dark",
"alert",
"malahit",
"attantion"
];
// src/core/classnames.ts
var BASE_CLASS = "tach-typography";
var join = (...parts) => parts.filter(Boolean).join(" ");
var tachTypographyClassName = ({
variant = "Body",
color = "primary",
weight = "normal",
clickable = false,
className
} = {}) => {
return join(
BASE_CLASS,
`${BASE_CLASS}--${variant}`,
`${BASE_CLASS}--color-${color}`,
weight === "bold" && `${BASE_CLASS}--bold`,
clickable && `${BASE_CLASS}--pointer`,
className
);
};
var tachTypographyClassList = (options = {}) => {
return tachTypographyClassName(options).split(" ").filter(Boolean);
};
// src/core/ellipsis.ts
var tachTypographyEllipsisStyle = (ellipsis) => {
if (!ellipsis) {
return void 0;
}
const rows = typeof ellipsis === "object" ? ellipsis.rows ?? 1 : 1;
return {
overflow: "hidden",
textOverflow: "ellipsis",
display: "-webkit-box",
WebkitBoxOrient: "vertical",
WebkitLineClamp: rows
};
};
export { TYPOGRAPHY_COLORS, TYPOGRAPHY_VARIANTS, tachTypographyClassList, tachTypographyClassName, tachTypographyEllipsisStyle };
//# sourceMappingURL=index.js.map
//# sourceMappingURL=index.js.map

View File

@@ -0,0 +1 @@
{"version":3,"sources":["../../src/core/types.ts","../../src/core/classnames.ts","../../src/core/ellipsis.ts"],"names":[],"mappings":";AAAO,IAAM,mBAAA,GAAsB;AAAA,EACjC,YAAA;AAAA,EACA,QAAA;AAAA,EACA,QAAA;AAAA,EACA,QAAA;AAAA,EACA,UAAA;AAAA,EACA,MAAA;AAAA,EACA,QAAA;AAAA,EACA,aAAA;AAAA,EACA,mBAAA;AAAA,EACA,UAAA;AAAA,EACA,SAAA;AAAA,EACA,UAAA;AAAA,EACA,UAAA;AAAA,EACA,UAAA;AAAA,EACA,cAAA;AAAA,EACA,eAAA;AAAA,EACA,eAAA;AAAA,EACA,gBAAA;AAAA,EACA,gBAAA;AAAA,EACA,gBAAA;AAAA,EACA,gBAAA;AAAA,EACA,eAAA;AAAA,EACA,kBAAA;AAAA,EACA;AACF;AAEO,IAAM,iBAAA,GAAoB;AAAA,EAC/B,SAAA;AAAA,EACA,WAAA;AAAA,EACA,UAAA;AAAA,EACA,YAAA;AAAA,EACA,MAAA;AAAA,EACA,OAAA;AAAA,EACA,MAAA;AAAA,EACA,OAAA;AAAA,EACA,SAAA;AAAA,EACA;AACF;;;ACpCA,IAAM,UAAA,GAAa,iBAAA;AAEnB,IAAM,IAAA,GAAO,IAAI,KAAA,KACf,KAAA,CAAM,OAAO,OAAO,CAAA,CAAE,KAAK,GAAG,CAAA;AAEzB,IAAM,0BAA0B,CAAC;AAAA,EACtC,OAAA,GAAU,MAAA;AAAA,EACV,KAAA,GAAQ,SAAA;AAAA,EACR,MAAA,GAAS,QAAA;AAAA,EACT,SAAA,GAAY,KAAA;AAAA,EACZ;AACF,CAAA,GAA4B,EAAC,KAAc;AACzC,EAAA,OAAO,IAAA;AAAA,IACL,UAAA;AAAA,IACA,CAAA,EAAG,UAAU,CAAA,EAAA,EAAK,OAAO,CAAA,CAAA;AAAA,IACzB,CAAA,EAAG,UAAU,CAAA,QAAA,EAAW,KAAK,CAAA,CAAA;AAAA,IAC7B,MAAA,KAAW,MAAA,IAAU,CAAA,EAAG,UAAU,CAAA,MAAA,CAAA;AAAA,IAClC,SAAA,IAAa,GAAG,UAAU,CAAA,SAAA,CAAA;AAAA,IAC1B;AAAA,GACF;AACF;AAEO,IAAM,uBAAA,GAA0B,CAAC,OAAA,GAAkC,EAAC,KAAgB;AACzF,EAAA,OAAO,wBAAwB,OAAO,CAAA,CACnC,MAAM,GAAG,CAAA,CACT,OAAO,OAAO,CAAA;AACnB;;;ACxBO,IAAM,2BAAA,GAA8B,CACzC,QAAA,KAC4B;AAC5B,EAAA,IAAI,CAAC,QAAA,EAAU;AACb,IAAA,OAAO,MAAA;AAAA,EACT;AAEA,EAAA,MAAM,OAAO,OAAO,QAAA,KAAa,QAAA,GAAW,QAAA,CAAS,QAAQ,CAAA,GAAI,CAAA;AAEjE,EAAA,OAAO;AAAA,IACL,QAAA,EAAU,QAAA;AAAA,IACV,YAAA,EAAc,UAAA;AAAA,IACd,OAAA,EAAS,aAAA;AAAA,IACT,eAAA,EAAiB,UAAA;AAAA,IACjB,eAAA,EAAiB;AAAA,GACnB;AACF","file":"index.js","sourcesContent":["export const TYPOGRAPHY_VARIANTS = [\n \"LargeTitle\",\n \"Title1\",\n \"Title2\",\n \"Title3\",\n \"Headline\",\n \"Body\",\n \"Inputs\",\n \"Subheadline\",\n \"FootnoteUnderline\",\n \"Footnote\",\n \"Caption\",\n \"Caption2\",\n \"AccentH1\",\n \"AccentH2\",\n \"AccentSubttl\",\n \"AccentSubttl2\",\n \"AccentCaption\",\n \"AccentCaption2\",\n \"AccentRegularM\",\n \"AccentRegularS\",\n \"AccentLargeTtl\",\n \"AppMediumBody\",\n \"AppMediumSubtext\",\n \"AppMediumSubtextUnderline\",\n] as const;\n\nexport const TYPOGRAPHY_COLORS = [\n \"primary\",\n \"secondary\",\n \"tertiary\",\n \"quaternary\",\n \"link\",\n \"white\",\n \"dark\",\n \"alert\",\n \"malahit\",\n \"attantion\",\n] as const;\n\nexport type TypographyVariant = (typeof TYPOGRAPHY_VARIANTS)[number];\nexport type TypographyColor = (typeof TYPOGRAPHY_COLORS)[number];\nexport type TypographyWeight = \"normal\" | \"bold\";\n\nexport interface TypographyClassOptions {\n variant?: TypographyVariant;\n color?: TypographyColor;\n weight?: TypographyWeight;\n clickable?: boolean;\n className?: string | undefined;\n}\n\nexport type EllipsisOptions =\n | boolean\n | {\n rows?: number;\n };\n\nexport interface TypographyRenderOptions extends TypographyClassOptions {\n ellipsis?: EllipsisOptions;\n}\n","import type { TypographyClassOptions } from \"./types\";\n\nconst BASE_CLASS = \"tach-typography\";\n\nconst join = (...parts: Array<string | undefined | null | false>): string =>\n parts.filter(Boolean).join(\" \");\n\nexport const tachTypographyClassName = ({\n variant = \"Body\",\n color = \"primary\",\n weight = \"normal\",\n clickable = false,\n className,\n}: TypographyClassOptions = {}): string => {\n return join(\n BASE_CLASS,\n `${BASE_CLASS}--${variant}`,\n `${BASE_CLASS}--color-${color}`,\n weight === \"bold\" && `${BASE_CLASS}--bold`,\n clickable && `${BASE_CLASS}--pointer`,\n className,\n );\n};\n\nexport const tachTypographyClassList = (options: TypographyClassOptions = {}): string[] => {\n return tachTypographyClassName(options)\n .split(\" \")\n .filter(Boolean);\n};\n","import type { EllipsisOptions } from \"./types\";\n\ntype StyleRecord = Record<string, string | number>;\n\nexport const tachTypographyEllipsisStyle = (\n ellipsis?: EllipsisOptions,\n): StyleRecord | undefined => {\n if (!ellipsis) {\n return undefined;\n }\n\n const rows = typeof ellipsis === \"object\" ? ellipsis.rows ?? 1 : 1;\n\n return {\n overflow: \"hidden\",\n textOverflow: \"ellipsis\",\n display: \"-webkit-box\",\n WebkitBoxOrient: \"vertical\",\n WebkitLineClamp: rows,\n };\n};\n"]}

Some files were not shown because too many files have changed in this diff Show More