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
2 changes: 1 addition & 1 deletion .cursor/rules/ls-foundry-core.mdc
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ Always bump `shared-types` first when changing layout/DPI APIs; keep dependent p

| Package | Version | Docs |
|---------|---------|------|
| `@jeffgo10/helpers` | 1.2.0 | `packages/helpers/` (`./image`, `./gestures`, `./browser`, `./clipboard`, `./text`) |
| `@jeffgo10/helpers` | 1.3.0 | `packages/helpers/` (`./image`, `./gestures`, `./browser`, `./clipboard`, `./text`, `./brand`) |
| `@jeffgo10/history` | 1.0.0 | `packages/history/` — generic undo/redo snapshot stacks |
| `@jeffgo10/three-d-label-customizer` | 0.1.0 | `docs/three-d-label-customizer/` |
| `@jeffgo10/gl-viewer` | 1.0.0 | `packages/gl-viewer/` (consumer: **LiteShadeMedia**) |
Expand Down
47 changes: 47 additions & 0 deletions packages/helpers/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ Published subpaths:
- **`@jeffgo10/helpers/browser`** — in-app WebView detection for export UX
- **`@jeffgo10/helpers/clipboard`** — `useCopyLink` React hook for copy-to-clipboard UI
- **`@jeffgo10/helpers/text`** — `useScrambleReveal` React hook for ticker-style text reveal
- **`@jeffgo10/helpers/brand`** — LiteShade Media mark + wordmark as inline SVG React components

Source: [github.com/jeffgo10/ls-foundry](https://github.com/jeffgo10/ls-foundry) (`packages/helpers`).

Expand Down Expand Up @@ -157,6 +158,52 @@ Provider + hook only — no presentational text component. Mark consumer compone

**Peer dependency:** `react` ^18 or ^19.

## `@jeffgo10/helpers/brand`

```tsx
import { ScrambleRevealProvider } from "@jeffgo10/helpers/text";
import {
LiteShadeMark,
LiteShadeWordmark,
LiteShadeBrand,
} from "@jeffgo10/helpers/brand";

// Consumer provides the provider once (app shell / layout) — brand does not.
<ScrambleRevealProvider>
<LiteShadeMark size={24} color="#fff" />
<LiteShadeWordmark color="currentColor" />
<LiteShadeBrand color="#ffffff" size={24} />
</ScrambleRevealProvider>
```

Inline SVG converted from LiteShadeMedia `public/lsm-white.svg` / `lsm-black.svg`. Paths use `fill="currentColor"` so `color` (or CSS `color`) themes the mark without white/black asset variants. Three paths stay separate with stable hooks:

| Selector | Geometry |
|----------|----------|
| `[data-lsm-mark]` / `.lsm-mark` | Path group |
| `[data-lsm-path="outer"]` | Outer hex shell |
| `[data-lsm-path="inner-a"]` | Upper / right facet |
| `[data-lsm-path="inner-b"]` | Lower / left facet |

**Wordmark text is fixed** to `LITESHADEMEDIA` (no `label` prop). `LiteShadeWordmark` uses `useScrambleReveal` from `@jeffgo10/helpers/text` internally — real string stays in a visually-hidden node for SEO; the animated layer is `aria-hidden`. Forward scramble timing via wordmark props / `wordmarkProps` (`delayMs`, `disabled`, …). Do **not** expect `ScrambleRevealProvider` inside this package.

**Mark fluorescent blink (start only):** on mount, each of the three paths runs its own randomized CSS `@keyframes` flicker (`step-end`, unsynced duration/delay) for ~`blinkDurationMs` (default `2000`), then settles to full opacity — like a tube warming up. Same skip gates as scramble (provider / reduced-motion / bots). Disable with `blinkDisabled` or `markProps={{ blinkDisabled: true }}`.

| Prop | Components | Default | Notes |
|------|------------|---------|--------|
| `color` | Mark / Wordmark / Brand | `currentColor` | Sets CSS `color` |
| `size` | Mark / Brand | `24` | SVG width & height |
| `showMark` / `showWordmark` | Brand | `true` | Toggle parts |
| `gap` | Brand | `0.5rem` | Flex gap |
| `markProps` / `wordmarkProps` | Brand | — | Forwarded to children (blink / scramble options) |
| `as` | Wordmark | `span` | `span` \| `div` \| `p` |
| `delayMs` / `disabled` / … | Wordmark | see `./text` | `UseScrambleRevealOptions` (text is not overridable) |
| `blinkDisabled` | Mark | `false` | Skip fluorescent turn-on |
| `blinkDurationMs` | Mark | `2000` | Base flicker window before settle |
| `blinkDelayMs` | Mark | `0` | Delay before flicker starts |

**Peer dependency:** `react` ^18 or ^19. Framework-agnostic — no Next.js / `/public` assets.

## License

MIT
15 changes: 15 additions & 0 deletions packages/helpers/jest.config.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,25 @@ module.exports = {
rootDir: ".",
testEnvironment: "jsdom",
moduleNameMapper: {
"^@jeffgo10/helpers/text$": "<rootDir>/src/text/index.ts",
"^@jeffgo10/helpers/brand$": "<rootDir>/src/brand/index.ts",
"^@ls-foundry/test-utils$": path.join(__dirname, "../test-utils/src/index.ts"),
"^@ls-foundry/test-utils/(.*)$": path.join(__dirname, "../test-utils/src/$1"),
},
coverageThreshold: {
"./src/brand/LiteShadeMark.tsx": threshold90,
"./src/brand/LiteShadeWordmark.tsx": threshold90,
"./src/brand/LiteShadeBrand.tsx": threshold90,
"./src/brand/paths.ts": threshold90,
"./src/brand/fluorescentBlink.ts": {
...threshold90,
// RNG opacity / Math.min settle branches are partially probabilistic.
branches: 80,
},
"./src/brand/useFluorescentBlink.ts": {
...threshold90,
branches: 80,
},
"./src/gestures/geometry.ts": threshold90,
"./src/gestures/usePointerTransformGestures.ts": {
...threshold90,
Expand Down
7 changes: 6 additions & 1 deletion packages/helpers/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@jeffgo10/helpers",
"version": "1.2.0",
"version": "1.3.0",
"description": "Shared helper utilities for ls-foundry packages",
"license": "MIT",
"repository": {
Expand All @@ -11,6 +11,11 @@
"type": "module",
"sideEffects": false,
"exports": {
"./brand": {
"types": "./dist/brand.d.ts",
"import": "./dist/brand.js",
"require": "./dist/brand.cjs"
},
"./browser": {
"types": "./dist/browser.d.ts",
"import": "./dist/browser.js",
Expand Down
85 changes: 85 additions & 0 deletions packages/helpers/src/brand/LiteShadeBrand.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import type { CSSProperties, HTMLAttributes } from "react";

import {
LiteShadeMark,
type LiteShadeMarkProps,
} from "./LiteShadeMark";
import {
LiteShadeWordmark,
type LiteShadeWordmarkProps,
} from "./LiteShadeWordmark";

export type LiteShadeBrandProps = Omit<
HTMLAttributes<HTMLDivElement>,
"children" | "color"
> & {
/** Applied to mark + wordmark (and root `color`). Default `"currentColor"`. */
color?: string;
/** Mark size (width & height). Default `24` (~1.5rem). */
size?: number | string;
/** Fixed brand string — not overridable (always `LITESHADEMEDIA`). */
label?: never;
/** Render the SVG mark. Default `true`. */
showMark?: boolean;
/** Render the wordmark. Default `true`. */
showWordmark?: boolean;
/** Flex gap between mark and wordmark. Default `"0.5rem"`. */
gap?: number | string;
/** Extra props for {@link LiteShadeMark} (animation classNames, etc.). */
markProps?: Omit<LiteShadeMarkProps, "size" | "color">;
/**
* Extra props for {@link LiteShadeWordmark} (scramble options, className, …).
* Label is fixed to `LITESHADEMEDIA` and cannot be overridden.
*/
wordmarkProps?: Omit<LiteShadeWordmarkProps, "color">;
};

/**
* Combined LiteShade mark + wordmark for headers / nav.
* Layout matches LiteShadeMedia TopNav brand block (flex, gap, tracking).
* Wordmark always reads `LITESHADEMEDIA` with scramble via `useScrambleReveal`.
* Provide {@link ScrambleRevealProvider} in the consumer app.
*/
export function LiteShadeBrand({
color = "currentColor",
size = 24,
className,
showMark = true,
showWordmark = true,
gap = "0.5rem",
markProps,
wordmarkProps,
style,
label: _forbiddenLabel,
...rest
}: LiteShadeBrandProps) {
void _forbiddenLabel;
const mergedStyle: CSSProperties = {
display: "flex",
alignItems: "center",
gap,
color,
fontSize: "0.875rem",
letterSpacing: "0.25em",
...style,
};

const markDecorative = showWordmark;

return (
<div className={className} style={mergedStyle} data-lsm-brand="" {...rest}>
{showMark ? (
<LiteShadeMark
size={size}
color={color}
title={markDecorative ? undefined : "LiteShadeMedia"}
aria-hidden={markDecorative ? true : undefined}
{...markProps}
/>
) : null}
{showWordmark ? (
<LiteShadeWordmark color={color} {...wordmarkProps} />
) : null}
</div>
);
}
Loading
Loading