Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,12 @@ package-lock.json
/dist
*.tsbuildinfo

# ---------------------------------------------------------------------------
# Storybook
# ---------------------------------------------------------------------------
storybook-static/
*.log.json

# ---------------------------------------------------------------------------
# Python
# ---------------------------------------------------------------------------
Expand Down
3 changes: 2 additions & 1 deletion .vscode/extensions.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"charliermarsh.ruff",
"ms-vscode.powershell",
"ms-python.python",
"ms-vscode.vscode-typescript-next"
"ms-vscode.vscode-typescript-next",
"highagency.pencildev"
]
}
56 changes: 23 additions & 33 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,10 @@
"docker:test:image": "bash tests/container-image-test.sh",
"docker:test:vulnerability": "bash tests/container-vulnerability-scan.sh",
"commit": "bunx cz",
"storybook": "bun run --filter @tale/platform storybook",
"storybook": "bunx turbo run storybook",
"storybook:ui": "bun run --filter @tale/ui storybook",
"storybook:webui": "bun run --filter @tale/webui storybook",
"storybook:platform": "bun run --filter @tale/platform storybook",
"gen": "bunx --bun plop --plopfile plopfile.ts",
"gen:react-service": "bun run gen react-service",
"gen:react-package": "bun run gen react-package",
Expand Down
11 changes: 10 additions & 1 deletion packages/ui/.oxlintrc.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,13 @@
{
"$schema": "../../node_modules/oxlint/configuration_schema.json",
"extends": ["../../.oxlintrc.json"]
"extends": ["../../.oxlintrc.json"],
"overrides": [
{
"files": ["src/markdown/**/*.{ts,tsx}"],
"rules": {
"typescript/no-unsafe-type-assertion": "off",
"typescript/consistent-return": "off"
}
}
]
}
35 changes: 35 additions & 0 deletions packages/ui/.storybook/preview.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,41 @@
import { withThemeByClassName } from '@storybook/addon-themes';
import type { Preview } from '@storybook/react';
import { useMemo } from 'react';
import type { DecoratorFunction } from 'storybook/internal/types';

import { ThemeContext } from '../src/theme';

import '../src/globals.css';
import '../src/markdown/globals.css';

/**
* Bridge addon-themes' html-class toggle into the React `ThemeContext` so
* components reading `useTheme()` (CodeBlock, Mermaid, etc.) see the same
* state Storybook shows. Without this, `resolvedTheme` is pinned to
* `'light'` even when the iframe has `.dark` on `<html>`.
*/
function WithTheme({
Story,
context,
}: {
Story: Parameters<DecoratorFunction>[0];
context: Parameters<DecoratorFunction>[1];
}) {
const resolvedTheme = context.globals.theme === 'dark' ? 'dark' : 'light';
const value = useMemo(
() => ({
theme: resolvedTheme,
resolvedTheme,
setTheme: () => {},
}),
[resolvedTheme],
);
return (
<ThemeContext.Provider value={value}>
<Story />
</ThemeContext.Provider>
);
}

const preview: Preview = {
parameters: {
Expand All @@ -22,6 +56,7 @@ const preview: Preview = {
},
},
decorators: [
(Story, context) => <WithTheme Story={Story} context={context} />,
withThemeByClassName({
themes: { light: '', dark: 'dark' },
defaultTheme: 'light',
Expand Down
44 changes: 42 additions & 2 deletions packages/ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "@tale/ui",
"version": "0.1.0",
"private": true,
"description": "Shared Tale React component library",
"description": "Shared Tale React component library — primitives, theming, i18n + the markdown rendering pipeline (components, plugins, streaming).",
"type": "module",
"exports": {
".": "./src/index.ts",
Expand Down Expand Up @@ -37,6 +37,30 @@
"./cn": "./src/lib/cn.ts",
"./format": "./src/lib/format.ts",
"./i18n-tests": "./src/i18n-tests/index.ts",
"./use-resize-observer": "./src/hooks/use-resize-observer.ts",
"./markdown": "./src/markdown/markdown.tsx",
"./markdown/routed-markdown": "./src/markdown/routed-markdown.tsx",
"./markdown/shiki": "./src/markdown/shiki.ts",
"./markdown/code-block": "./src/markdown/code-block.tsx",
"./markdown/anchored-heading": "./src/markdown/anchored-heading.tsx",
"./markdown/extract-toc": "./src/markdown/extract-toc.ts",
"./markdown/reading-time": "./src/markdown/reading-time.ts",
"./markdown/components/callout": "./src/markdown/components/callout.tsx",
"./markdown/components/code-group": "./src/markdown/components/code-group.tsx",
"./markdown/components/tabs": "./src/markdown/components/tabs.tsx",
"./markdown/components/steps": "./src/markdown/components/steps.tsx",
"./markdown/components/cards": "./src/markdown/components/cards.tsx",
"./markdown/components/frame": "./src/markdown/components/frame.tsx",
"./markdown/components/accordion": "./src/markdown/components/accordion.tsx",
"./markdown/components/mermaid": "./src/markdown/components/mermaid.tsx",
"./markdown/components/registry": "./src/markdown/components/registry.tsx",
"./markdown/streaming/incremental-markdown": "./src/markdown/streaming/incremental-markdown.tsx",
"./markdown/streaming/remend-markdown": "./src/markdown/streaming/remend-markdown.ts",
"./markdown/streaming/normalize-html-blocks": "./src/markdown/streaming/normalize-html-blocks.ts",
"./markdown/streaming/find-block-split": "./src/markdown/streaming/find-block-split.ts",
"./markdown/plugins/micromark-cjk-attention": "./src/markdown/plugins/micromark-cjk-attention.ts",
"./markdown/types": "./src/markdown/types.ts",
"./markdown/globals.css": "./src/markdown/globals.css",
"./globals.css": "./src/globals.css",
"./tailwind-preset": "./tailwind-preset.ts"
},
Expand All @@ -59,10 +83,26 @@
"i18next": "26.0.4",
"i18next-icu": "2.4.3",
"lucide-react": "1.8.0",
"mermaid": "11.14.0",
"micromark": "4.0.2",
"micromark-core-commonmark": "2.0.3",
"micromark-util-classify-character": "2.0.1",
"micromark-util-types": "2.0.2",
"react-i18next": "17.0.2",
"tailwind-merge": "3.5.0"
"react-markdown": "10.1.0",
"rehype-raw": "7.0.0",
"rehype-sanitize": "6.0.0",
"remark-gfm": "4.0.1",
"shiki": "3.2.0",
"tailwind-merge": "3.5.0",
"unist-util-visit": "5.1.0"
},
"devDependencies": {
"@types/hast": "3.0.4"
},
"peerDependencies": {
"react": "*",
"react-dom": "*",
"vitest": "*"
},
"peerDependenciesMeta": {
Expand Down
8 changes: 8 additions & 0 deletions packages/ui/postcss.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/** @type {import('postcss-load-config').Config} */
const config = {
plugins: {
'@tailwindcss/postcss': {},
},
};

export default config;
7 changes: 5 additions & 2 deletions packages/ui/src/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -91,9 +91,12 @@
--color-bg-muted: #1a1a1a;
--color-bg-overlay: rgba(3, 7, 18, 0.8);

/* Body prose lifts a notch (gray-300 vs gray-400) for legibility on dark
backgrounds. fg-subtle slides up to fill the lower-contrast slot for
captions / muted metadata. */
--color-fg-base: #f3f4f6;
--color-fg-muted: #9ca3af;
--color-fg-subtle: #6b7280;
--color-fg-muted: #d1d5db;
--color-fg-subtle: #9ca3af;
--color-fg-inverse: #030712;

--color-border-base: #404040;
Expand Down
39 changes: 39 additions & 0 deletions packages/ui/src/hooks/use-resize-observer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { useEffect, useRef } from 'react';

type ResizeCallback = (entry: ResizeObserverEntry, target: Element) => void;

/**
* Subscribe a stable callback to ResizeObserver entries on the given element.
*
* The callback is stored in a ref so it can change every render without
* tearing down the observer; only `target` toggles re-create the observer.
* Pass `null`/`undefined` to disable. The callback fires once when the
* observer first attaches (per the ResizeObserver spec) — useful for
* initial-fit logic that needs the post-layout size.
*
* Example:
* ```ts
* const ref = useRef<HTMLDivElement>(null);
* useResizeObserver(ref.current, (entry) => {
* setSize({ w: entry.contentRect.width, h: entry.contentRect.height });
* });
* ```
*/
export function useResizeObserver<T extends Element>(
target: T | null | undefined,
callback: ResizeCallback,
): void {
const callbackRef = useRef(callback);
callbackRef.current = callback;

useEffect(() => {
if (!target) return undefined;
const observer = new ResizeObserver((entries) => {
for (const entry of entries) {
callbackRef.current(entry, entry.target);
}
});
observer.observe(target);
return () => observer.disconnect();
}, [target]);
}
Loading
Loading