Add joplin web article (#9)

This commit is contained in:
deytenit authored and GitHub committed 2026-03-20 01:32:26 +03:00
1 parent 90bf68c4c8
commit 575959ffa2
38 files changed
+1043 -554

No files matched your search

@@ -51,9 +51,6 @@ const MarkdownComponent = memo(function Markdown({ children, className }: Markdo
* <Markdown.H1>Title</Markdown.H1>
* <Markdown.P>Content goes here.</Markdown.P>
* </Markdown>
*
* // Or with Astro:
* <Content components={MARKDOWN_COMPONENTS} />
* ```
*/
export const Markdown = Object.assign(MarkdownComponent, {
@@ -24,7 +24,7 @@ import { MarkdownUL } from './MarkdownUL/MarkdownUL';
import type React from 'react';
/**
* A map of HTML tags to Markdown components for use with MDX or Astro Content.
* A map of HTML tags to Markdown components for use with MDX or other Markdown systems.
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const MARKDOWN_COMPONENTS: Record<string, React.ComponentType<any>> = {
@@ -32,7 +32,7 @@ describe('PageHead', () => {
it('applies the correct classes for the strategy', () => {
render(<PageHead heading="Test" strategy="always-collapsed-fixed" />);
const header = screen.getByRole('banner');
expect(header).toHaveClass('fixed');
expect(header).toHaveClass('sticky');
expect(header).toHaveClass('shadow-md');
});
});
@@ -1,5 +1,5 @@
import { cn, useScroll, useResizeObserver } from '@ermnvldmr/stl';
import React, { type ReactNode, useRef, useEffect, useState } from 'react';
import { cn, useScroll } from '@ermnvldmr/stl';
import React, { type ReactNode, useEffect, useState } from 'react';
import { Header } from '../../Header/Header';
import { HStack } from '../../HStack/HStack';
@@ -68,8 +68,6 @@ export const PageHead = ({
}: PageHeadProps) => {
const { y: scrollY } = useScroll();
const [isCollapsed, setIsCollapsed] = useState(strategy.includes('always-collapsed'));
const headerRef = useRef<HTMLDivElement>(null);
const { height } = useResizeObserver(headerRef);
useEffect(() => {
if (strategy === 'collapsible-sticky') {
@@ -78,7 +76,7 @@ export const PageHead = ({
}, [scrollY, strategy]);
const rootClasses = cn(
'page-head w-full transition-all duration-200 bg-[var(--rb-background)] fixed top-0 z-10 border-b border-[var(--rb-outline)]/10',
'page-head w-full transition-all duration-200 bg-[var(--rb-background)] sticky top-0 z-10 border-b border-[var(--rb-outline)]/10',
{
'shadow-md bg-[var(--rb-background)]/90 backdrop-blur-md': isCollapsed,
},
@@ -124,28 +122,25 @@ export const PageHead = ({
);
return (
<>
<header ref={headerRef} className={rootClasses}>
<div
className={cn(
'grid transition-[grid-template-rows] duration-300 ease-in-out',
isCollapsed ? 'grid-rows-[0fr]' : 'grid-rows-[1fr]'
)}
inert={isCollapsed || undefined}
>
<div className="overflow-hidden">{expandedContent}</div>
</div>
<div
className={cn(
'grid transition-[grid-template-rows] duration-300 ease-in-out',
isCollapsed ? 'grid-rows-[1fr]' : 'grid-rows-[0fr]'
)}
inert={!isCollapsed || undefined}
>
<div className="overflow-hidden">{collapsedContent}</div>
</div>
</header>
<div style={{ height }} />
</>
<header className={rootClasses}>
<div
className={cn(
'grid transition-[grid-template-rows] duration-300 ease-in-out',
isCollapsed ? 'grid-rows-[0fr]' : 'grid-rows-[1fr]'
)}
inert={isCollapsed || undefined}
>
<div className="overflow-hidden">{expandedContent}</div>
</div>
<div
className={cn(
'grid transition-[grid-template-rows] duration-300 ease-in-out',
isCollapsed ? 'grid-rows-[1fr]' : 'grid-rows-[0fr]'
)}
inert={!isCollapsed || undefined}
>
<div className="overflow-hidden">{collapsedContent}</div>
</div>
</header>
);
};
+25 -5
View File
@@ -132,29 +132,49 @@ describe('components/Text', () => {
// TDD: fails before implementation (old code uses text-[14px], not text-rb-body-m)
it('applies text-rb-{type}-{size} class', () => {
render(<Text size="m" type="body">Content</Text>);
render(
<Text size="m" type="body">
Content
</Text>
);
expect(screen.getByText('Content')).toHaveClass('text-rb-body-m');
});
it('does not apply legacy px-based font-size classes', () => {
render(<Text size="m" type="body">Content</Text>);
render(
<Text size="m" type="body">
Content
</Text>
);
expect(screen.getByText('Content')).not.toHaveClass('text-[14px]');
expect(screen.getByText('Content')).not.toHaveClass('leading-[20px]');
});
// Regression guards: pass before and after
it('applies font-medium for label type', () => {
render(<Text size="m" type="label">Label</Text>);
render(
<Text size="m" type="label">
Label
</Text>
);
expect(screen.getByText('Label')).toHaveClass('font-medium');
});
it('applies font-sans for body type', () => {
render(<Text size="m" type="body">Body</Text>);
render(
<Text size="m" type="body">
Body
</Text>
);
expect(screen.getByText('Body')).toHaveClass('font-sans');
});
it('applies font-serif for display type', () => {
render(<Text size="l" type="display">Display</Text>);
render(
<Text size="l" type="display">
Display
</Text>
);
expect(screen.getByText('Display')).toHaveClass('font-serif');
});
});
+13 -1
View File
@@ -119,7 +119,19 @@ const TextComponent = forwardRef<HTMLElement, TextProps>(function Text(
{...props}
ref={internalRef}
className={cn(
getTypographyClassNames({ type, size, color, bold, italic, underline, strike, align, wrap, overflow, maxLines }),
getTypographyClassNames({
type,
size,
color,
bold,
italic,
underline,
strike,
align,
wrap,
overflow,
maxLines,
}),
delay !== undefined &&
(isVisible
? 'animate-in fade-in slide-in-from-bottom-2 duration-1000 ease-out'
@@ -7,8 +7,12 @@ describe('lib/typography/getTypographyClassNames', () => {
});
it('returns correct class for each type/size combination', () => {
expect(getTypographyClassNames({ type: 'display', size: 'l' })).toContain('text-rb-display-l');
expect(getTypographyClassNames({ type: 'headline', size: 'm' })).toContain('text-rb-headline-m');
expect(getTypographyClassNames({ type: 'display', size: 'l' })).toContain(
'text-rb-display-l'
);
expect(getTypographyClassNames({ type: 'headline', size: 'm' })).toContain(
'text-rb-headline-m'
);
expect(getTypographyClassNames({ type: 'title', size: 's' })).toContain('text-rb-title-s');
expect(getTypographyClassNames({ type: 'body', size: 'l' })).toContain('text-rb-body-l');
expect(getTypographyClassNames({ type: 'label', size: 'm' })).toContain('text-rb-label-m');
@@ -54,7 +58,9 @@ describe('lib/typography/getTypographyClassNames', () => {
it('applies each color variant', () => {
expect(getTypographyClassNames({ color: 'muted' })).toContain('text-[var(--rb-muted-text)]');
expect(getTypographyClassNames({ color: 'primary' })).toContain('text-[var(--rb-primary-text)]');
expect(getTypographyClassNames({ color: 'primary' })).toContain(
'text-[var(--rb-primary-text)]'
);
expect(getTypographyClassNames({ color: 'error' })).toContain('text-[var(--rb-error-text)]');
expect(getTypographyClassNames({ color: 'inherit' })).toContain('text-inherit');
});
+104 -35
View File
@@ -191,29 +191,62 @@
@theme {
/* display */
--text-rb-display-l: 3.5625rem; --text-rb-display-l--line-height: 4rem; --text-rb-display-l--letter-spacing: -0.015625rem;
--text-rb-display-m: 2.8125rem; --text-rb-display-m--line-height: 3.25rem; --text-rb-display-m--letter-spacing: 0rem;
--text-rb-display-s: 2.25rem; --text-rb-display-s--line-height: 2.75rem; --text-rb-display-s--letter-spacing: 0rem;
--text-rb-display-l: 3.5625rem;
--text-rb-display-l--line-height: 4rem;
--text-rb-display-l--letter-spacing: -0.015625rem;
--text-rb-display-m: 2.8125rem;
--text-rb-display-m--line-height: 3.25rem;
--text-rb-display-m--letter-spacing: 0rem;
--text-rb-display-s: 2.25rem;
--text-rb-display-s--line-height: 2.75rem;
--text-rb-display-s--letter-spacing: 0rem;
/* headline */
--text-rb-headline-l: 2rem; --text-rb-headline-l--line-height: 2.5rem; --text-rb-headline-l--letter-spacing: 0rem;
--text-rb-headline-m: 1.75rem; --text-rb-headline-m--line-height: 2.25rem; --text-rb-headline-m--letter-spacing: 0rem;
--text-rb-headline-s: 1.5rem; --text-rb-headline-s--line-height: 2rem; --text-rb-headline-s--letter-spacing: 0rem;
--text-rb-headline-l: 2rem;
--text-rb-headline-l--line-height: 2.5rem;
--text-rb-headline-l--letter-spacing: 0rem;
--text-rb-headline-m: 1.75rem;
--text-rb-headline-m--line-height: 2.25rem;
--text-rb-headline-m--letter-spacing: 0rem;
--text-rb-headline-s: 1.5rem;
--text-rb-headline-s--line-height: 2rem;
--text-rb-headline-s--letter-spacing: 0rem;
/* title */
--text-rb-title-l: 1.375rem; --text-rb-title-l--line-height: 1.75rem; --text-rb-title-l--letter-spacing: 0rem;
--text-rb-title-m: 1rem; --text-rb-title-m--line-height: 1.5rem; --text-rb-title-m--letter-spacing: 0.009375rem;
--text-rb-title-s: 0.875rem; --text-rb-title-s--line-height: 1.25rem; --text-rb-title-s--letter-spacing: 0.00625rem;
--text-rb-title-l: 1.375rem;
--text-rb-title-l--line-height: 1.75rem;
--text-rb-title-l--letter-spacing: 0rem;
--text-rb-title-m: 1rem;
--text-rb-title-m--line-height: 1.5rem;
--text-rb-title-m--letter-spacing: 0.009375rem;
--text-rb-title-s: 0.875rem;
--text-rb-title-s--line-height: 1.25rem;
--text-rb-title-s--letter-spacing: 0.00625rem;
/* body — baseline raised: m = 1rem (was 14px/0.875rem) */
--text-rb-body-l: 1.125rem; --text-rb-body-l--line-height: 1.75rem; --text-rb-body-l--letter-spacing: 0.03125rem;
--text-rb-body-m: 1rem; --text-rb-body-m--line-height: 1.5rem; --text-rb-body-m--letter-spacing: 0.015625rem;
--text-rb-body-s: 0.875rem; --text-rb-body-s--line-height: 1.25rem; --text-rb-body-s--letter-spacing: 0.025rem;
--text-rb-body-l: 1.125rem;
--text-rb-body-l--line-height: 1.75rem;
--text-rb-body-l--letter-spacing: 0.03125rem;
--text-rb-body-m: 1rem;
--text-rb-body-m--line-height: 1.5rem;
--text-rb-body-m--letter-spacing: 0.015625rem;
--text-rb-body-s: 0.875rem;
--text-rb-body-s--line-height: 1.25rem;
--text-rb-body-s--letter-spacing: 0.025rem;
/* label */
--text-rb-label-l: 0.875rem; --text-rb-label-l--line-height: 1.25rem; --text-rb-label-l--letter-spacing: 0.00625rem; --text-rb-label-l--font-weight: 500;
--text-rb-label-m: 0.75rem; --text-rb-label-m--line-height: 1rem; --text-rb-label-m--letter-spacing: 0.03125rem; --text-rb-label-m--font-weight: 500;
--text-rb-label-s: 0.6875rem; --text-rb-label-s--line-height: 1rem; --text-rb-label-s--letter-spacing: 0.03125rem; --text-rb-label-s--font-weight: 500;
--text-rb-label-l: 0.875rem;
--text-rb-label-l--line-height: 1.25rem;
--text-rb-label-l--letter-spacing: 0.00625rem;
--text-rb-label-l--font-weight: 500;
--text-rb-label-m: 0.75rem;
--text-rb-label-m--line-height: 1rem;
--text-rb-label-m--letter-spacing: 0.03125rem;
--text-rb-label-m--font-weight: 500;
--text-rb-label-s: 0.6875rem;
--text-rb-label-s--line-height: 1rem;
--text-rb-label-s--letter-spacing: 0.03125rem;
--text-rb-label-s--font-weight: 500;
}
:root {
@@ -285,34 +318,70 @@
@layer components {
/* ── Scale parents — set CSS vars for all type children below ── */
.rb-scale-s {
--rb-sz-display: var(--text-rb-display-s); --rb-lh-display: var(--text-rb-display-s--line-height);
--rb-sz-headline: var(--text-rb-headline-s); --rb-lh-headline: var(--text-rb-headline-s--line-height);
--rb-sz-title: var(--text-rb-title-s); --rb-lh-title: var(--text-rb-title-s--line-height);
--rb-sz-body: var(--text-rb-body-s); --rb-lh-body: var(--text-rb-body-s--line-height);
--rb-sz-label: var(--text-rb-label-s); --rb-lh-label: var(--text-rb-label-s--line-height);
--rb-sz-display: var(--text-rb-display-s);
--rb-lh-display: var(--text-rb-display-s--line-height);
--rb-sz-headline: var(--text-rb-headline-s);
--rb-lh-headline: var(--text-rb-headline-s--line-height);
--rb-sz-title: var(--text-rb-title-s);
--rb-lh-title: var(--text-rb-title-s--line-height);
--rb-sz-body: var(--text-rb-body-s);
--rb-lh-body: var(--text-rb-body-s--line-height);
--rb-sz-label: var(--text-rb-label-s);
--rb-lh-label: var(--text-rb-label-s--line-height);
}
.rb-scale-m {
--rb-sz-display: var(--text-rb-display-m); --rb-lh-display: var(--text-rb-display-m--line-height);
--rb-sz-headline: var(--text-rb-headline-m); --rb-lh-headline: var(--text-rb-headline-m--line-height);
--rb-sz-title: var(--text-rb-title-m); --rb-lh-title: var(--text-rb-title-m--line-height);
--rb-sz-body: var(--text-rb-body-m); --rb-lh-body: var(--text-rb-body-m--line-height);
--rb-sz-label: var(--text-rb-label-m); --rb-lh-label: var(--text-rb-label-m--line-height);
--rb-sz-display: var(--text-rb-display-m);
--rb-lh-display: var(--text-rb-display-m--line-height);
--rb-sz-headline: var(--text-rb-headline-m);
--rb-lh-headline: var(--text-rb-headline-m--line-height);
--rb-sz-title: var(--text-rb-title-m);
--rb-lh-title: var(--text-rb-title-m--line-height);
--rb-sz-body: var(--text-rb-body-m);
--rb-lh-body: var(--text-rb-body-m--line-height);
--rb-sz-label: var(--text-rb-label-m);
--rb-lh-label: var(--text-rb-label-m--line-height);
}
.rb-scale-l {
--rb-sz-display: var(--text-rb-display-l); --rb-lh-display: var(--text-rb-display-l--line-height);
--rb-sz-headline: var(--text-rb-headline-l); --rb-lh-headline: var(--text-rb-headline-l--line-height);
--rb-sz-title: var(--text-rb-title-l); --rb-lh-title: var(--text-rb-title-l--line-height);
--rb-sz-body: var(--text-rb-body-l); --rb-lh-body: var(--text-rb-body-l--line-height);
--rb-sz-label: var(--text-rb-label-l); --rb-lh-label: var(--text-rb-label-l--line-height);
--rb-sz-display: var(--text-rb-display-l);
--rb-lh-display: var(--text-rb-display-l--line-height);
--rb-sz-headline: var(--text-rb-headline-l);
--rb-lh-headline: var(--text-rb-headline-l--line-height);
--rb-sz-title: var(--text-rb-title-l);
--rb-lh-title: var(--text-rb-title-l--line-height);
--rb-sz-body: var(--text-rb-body-l);
--rb-lh-body: var(--text-rb-body-l--line-height);
--rb-sz-label: var(--text-rb-label-l);
--rb-lh-label: var(--text-rb-label-l--line-height);
}
/* ── Type children — declare type, read from parent scale vars ── */
/* Fallback (second arg to var()) applies m-size when no rb-scale-* parent exists */
.rb-text-display { font-family: var(--font-serif); font-size: var(--rb-sz-display, var(--text-rb-display-m)); line-height: var(--rb-lh-display, var(--text-rb-display-m--line-height)); }
.rb-text-headline { font-family: var(--font-serif); font-size: var(--rb-sz-headline, var(--text-rb-headline-m)); line-height: var(--rb-lh-headline, var(--text-rb-headline-m--line-height)); }
.rb-text-title { font-family: var(--font-sans); font-size: var(--rb-sz-title, var(--text-rb-title-m)); line-height: var(--rb-lh-title, var(--text-rb-title-m--line-height)); }
.rb-text-body { font-family: var(--font-sans); font-size: var(--rb-sz-body, var(--text-rb-body-m)); line-height: var(--rb-lh-body, var(--text-rb-body-m--line-height)); }
.rb-text-label { font-family: var(--font-sans); font-weight: 500; font-size: var(--rb-sz-label, var(--text-rb-label-m)); line-height: var(--rb-lh-label, var(--text-rb-label-m--line-height)); }
.rb-text-display {
font-family: var(--font-serif);
font-size: var(--rb-sz-display, var(--text-rb-display-m));
line-height: var(--rb-lh-display, var(--text-rb-display-m--line-height));
}
.rb-text-headline {
font-family: var(--font-serif);
font-size: var(--rb-sz-headline, var(--text-rb-headline-m));
line-height: var(--rb-lh-headline, var(--text-rb-headline-m--line-height));
}
.rb-text-title {
font-family: var(--font-sans);
font-size: var(--rb-sz-title, var(--text-rb-title-m));
line-height: var(--rb-lh-title, var(--text-rb-title-m--line-height));
}
.rb-text-body {
font-family: var(--font-sans);
font-size: var(--rb-sz-body, var(--text-rb-body-m));
line-height: var(--rb-lh-body, var(--text-rb-body-m--line-height));
}
.rb-text-label {
font-family: var(--font-sans);
font-weight: 500;
font-size: var(--rb-sz-label, var(--text-rb-label-m));
line-height: var(--rb-lh-label, var(--text-rb-label-m--line-height));
}
}
@keyframes shimmer {