gendesign/frontend/src/lib/adminStyles.ts
lekss361 2416ed0db1
refactor(frontend): extract SectionLabel + EmptyState + adminStyles shared (#132)
* fix(docs): block javascript: URLs in renderMarkdown href (XSS guard)

Per audit batch #127 P2 security minor (issue #130).

## Risk

`renderMarkdown` substituted `[text](url)` → `<a href="...url...">` verbatim
без URL validation. Edge case: `[click](javascript:alert(1))` →
`<a href="javascript:alert(1)">click</a>` execute'нется при click.

Currently low risk (markdown source = build-time fs.readFileSync из
public/docs/, checked into repo), но защита-в-глубину: если когда-либо
markdown source станет user-supplied, vuln materialize.

## Fix

`frontend/src/app/docs/b2b-channels/renderMarkdown.ts`:

1. New `safeUrl(url)` function — allowlist `https://`, `http://`, `/`, `#`,
   `mailto:`. Everything else → `"#"`.
2. Link regex replacement switched from backreference string to callback
   so URL passes через `safeUrl` ДО injection в href.
3. URL unescape → safeUrl → re-escape (HTML escape applied per-line).

## Test cases verified

- `[ok](https://example.com)` → href=`https://example.com` 
- `[ok](/local)` → href=`/local` 
- `[ok](#anchor)` → href=`#anchor` 
- `[ok](mailto:foo@bar.com)` → href=`mailto:foo@bar.com` 
- `[bad](javascript:alert(1))` → href=`#` 
- `[bad](data:text/html,...)` → href=`#` 
- `[bad](vbscript:msgbox(1))` → href=`#` 

## Checks

- tsc: 0 errors
- lint: 0 warnings
- No existing `__tests__/renderMarkdown` to break

## Vault

`fixes/Bug_RenderMarkdown_JavascriptUrl_May14.md` — created.

Closes #130

* refactor(frontend): SectionLabel + EmptyState + adminStyles shared (fixup)

Per audit batch #127 P2 hygiene (issue #129). Previous commit лошил
файлы из-за pre-commit (видимо). Fixup-коммит с actual diff:

## New shared modules (3)

- frontend/src/components/ui/SectionLabel.tsx
- frontend/src/components/ui/EmptyState.tsx
- frontend/src/lib/adminStyles.ts

## Replacements

- SectionLabel: 12 inline usages → import (Overview/Land/Market/Environment Tab)
- EmptyState: 3 inline usages → import (Land/Market/Environment)
- adminStyles: 5 admin pages import cardStyle/labelStyle/inputStyle/th/td
- BulkGeoPanel: cardStyle only (preserves local labelStyle для span)
- leads/page.tsx: cardStyle с marginTop extend; th/td/inputStyle local (divergent)

## Visual regression: ZERO

All px/colors/CSS properties copied verbatim. Files с divergent values left local.

## Checks

- ruff (no files)
- prettier
- tsc + lint clean (per agent run)

Closes #129
Refs: #127

* docs(adminStyles): JSDoc warning про divergent local overrides

Per bot review #132 non-blocking minor — задокументировать почему
leads/page.tsx и BulkGeoPanel.tsx имеют local overrides, чтобы будущий
читатель не «унифицировал» обратно по ошибке.

Refs: #129, #132

---------

Co-authored-by: lekss361 <claudestars@proton.me>
2026-05-14 23:55:38 +03:00

49 lines
1.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Shared inline styles для admin pages.
*
* **Divergent local overrides — НЕ унифицировать:**
* - `frontend/src/app/admin/leads/page.tsx` — local `th` (padding "10px 12px",
* color "#374151"), local `td` (verticalAlign "top"), local `inputStyle`
* (fontSize 13, minWidth 160). Cause: разная плотность UI на leads dashboard.
* - `frontend/src/components/admin/BulkGeoPanel.tsx` — local `labelStyle` без
* `display: block` и `marginBottom`. Cause: labels на <span> внутри строки,
* block-display и margin сломали бы inline layout.
*
* При добавлении новой admin page — сначала проверь что shared values fit
* перед introducing local override.
*/
import type { CSSProperties } from "react";
export const cardStyle: CSSProperties = {
background: "#fff",
border: "1px solid #e6e8ec",
borderRadius: 12,
padding: 20,
};
export const labelStyle: CSSProperties = {
display: "block",
fontSize: 12,
color: "#5b6066",
marginBottom: 4,
textTransform: "uppercase",
letterSpacing: 0.4,
};
export const inputStyle: CSSProperties = {
padding: "8px 10px",
border: "1px solid #d1d5db",
borderRadius: 6,
fontSize: 14,
width: "100%",
boxSizing: "border-box",
};
export const th: CSSProperties = {
padding: "8px 10px",
textAlign: "left",
fontWeight: 600,
borderBottom: "1px solid #e6e8ec",
};
export const td: CSSProperties = { padding: "8px 10px" };