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
3 changes: 3 additions & 0 deletions apps/web/src/shared/components/Footer.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {Link} from 'react-router-dom';
import {INK_HUBS} from '../../features/cards/inkHubs';
import {APP_NAME, COLORS, FONTS, FONT_SIZES, INK_COLORS, SPACING, FONT_WEIGHT, LINE_HEIGHT} from '../constants';
import {RecurssiveCredit} from './RecurssiveCredit';

/**
* Site footer: legal/product navigation plus the Ravensburger Community Code
Expand Down Expand Up @@ -179,6 +180,8 @@ export function Footer() {
</a>
.
</p>

<RecurssiveCredit />
</div>
</footer>
);
Expand Down
14 changes: 14 additions & 0 deletions apps/web/src/shared/components/RecurssiveCredit.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import type {Meta, StoryObj} from '@storybook/react-vite';
import {RecurssiveCredit} from './RecurssiveCredit';

const meta: Meta<typeof RecurssiveCredit> = {
title: 'Components/RecurssiveCredit',
component: RecurssiveCredit,
parameters: {layout: 'centered'},
tags: ['autodocs'],
};

export default meta;
type Story = StoryObj<typeof RecurssiveCredit>;

export const Default: Story = {};
81 changes: 81 additions & 0 deletions apps/web/src/shared/components/RecurssiveCredit.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import {useState} from 'react';
import {useHover} from 'inkweave-ui/hooks/useHover';
import {focusedByKeyboard} from 'inkweave-ui/utils/focusedByKeyboard';
import {COLORS, DURATION, EASING, FONT_SIZES, FONT_WEIGHT, SPACING, TOUCH_TARGET} from '../constants';

/**
* "Developed and designed by Recurssive", the studio credit at the foot of the Footer.
*
* A hand-kept copy of the plain variant of Recurssive's shared footer credit, so a change to the
* shared snippet has to be carried over by hand. It is styled with Inkweave's tokens (owner,
* 2026-11-05) so it passes the design-token rules with no exemption: it rests in textMuted like
* the footer's links, lifts to text on hover or keyboard focus, and the mark turns the brand gold.
*
* minHeight TOUCH_TARGET: it stands alone on the footer's last row, not inside a sentence, so its
* 10px line would otherwise be its whole tap area (WCAG 2.5.8).
*
* rel="nofollow": the same link repeats in the footer of every page, and Google lists
* "widely distributed links in the footers or templates of various sites" under link spam.
*/

/**
* The Reflow mark, from Recurssive's own logo export (viewBox 0 0 120 80), its coordinates
* rounded to one decimal by script: at the footer's 10px a tenth of a unit is about 0.01px,
* and it keeps "User-facing JS" inside its budget. Do not hand-edit.
*/
const MARK_PATHS = [
'M58.1 78.1C51.2 65.7 44.4 53 37.6 41.1C37.1 40.2 37.1 39.1 37.6 38.2C44.4 26.3 51.2 13.6 58.1 1.2C59 -0.4 61 -0.4 61.9 1.2C68.7 13.6 75.5 26.3 82.3 38.2C82.9 39.1 82.9 40.2 82.3 41.1C75.6 53 68.7 65.7 61.9 78.1C61 79.7 59 79.7 58.1 78.1Z',
'M32.3 69.4C28.5 68.8 24.6 68.2 20.8 67.5C19.9 67.3 19.2 66.2 19.2 65V14.3C19.2 13.1 19.9 12 20.8 11.8C24.6 11.1 28.5 10.4 32.3 9.9C33.3 9.8 34 10.7 34.1 12.1V67.2C34.1 68.5 33.3 69.5 32.3 69.4Z',
'M7.5 21.4C11.6 20.5 15 28.7 15 39.6C15 50.6 11.6 58.8 7.5 57.9C3.3 56.9 0 48.7 0 39.6C0 30.6 3.3 22.3 7.5 21.4Z',
'M87.6 9.9C91.5 10.4 95.3 11.1 99.1 11.8C100 12 100.8 13.1 100.8 14.3V65C100.8 66.2 100 67.3 99.1 67.5C95.3 68.2 91.5 68.9 87.6 69.4C86.7 69.5 85.9 68.5 85.9 67.2V12.1C85.9 10.7 86.7 9.8 87.6 9.9Z',
'M119.9 39.6C119.9 48.7 116.6 56.9 112.5 57.9C108.4 58.8 105 50.6 105 39.6C105 28.7 108.4 20.5 112.5 21.4C116.6 22.3 119.9 30.6 119.9 39.6Z',
] as const;

export function RecurssiveCredit() {
const {hovered, hoverProps} = useHover();
const [focused, setFocused] = useState(false);
const lit = hovered || focused;
return (
<a
href="https://recurssive.com"
target="_blank"
rel="noopener nofollow"
{...hoverProps}
Comment thread
Doberjohn marked this conversation as resolved.
Resolved
// Keyboard focus only: a click's focus outlasts the click (the link opens a new tab, and
// focus is still here on the way back), which would leave the credit lit.
onFocus={(e) => setFocused(focusedByKeyboard(e.currentTarget))}
onBlur={() => setFocused(false)}
style={{
display: 'inline-flex',
alignItems: 'center',
minHeight: TOUCH_TARGET,
gap: `${SPACING.xs}px`,
color: lit ? COLORS.text : COLORS.textMuted,
fontSize: `${FONT_SIZES.xs}px`,
Comment thread
Doberjohn marked this conversation as resolved.
Resolved
textDecoration: 'none',
transition: `color ${DURATION.fast}ms ${EASING.snappy}`,
}}>
<svg
viewBox="0 0 120 80"
aria-hidden="true"
focusable="false"
style={{
Comment thread
Doberjohn marked this conversation as resolved.
Resolved
// Both sides set from the viewBox's 3:2: WebKit can size an inline SVG flex item
// with `width: auto` at its 300px default instead of from the ratio.
height: '1em',
width: '1.5em',
flexShrink: 0,
overflow: 'visible',
fill: lit ? COLORS.primary : 'currentColor',
transition: `fill ${DURATION.fast}ms ${EASING.snappy}`,
}}>
{MARK_PATHS.map((d) => (
<path key={d} d={d} />
))}
</svg>
<span>
Developed and designed by <span style={{fontWeight: FONT_WEIGHT.semibold}}>Recurssive</span>
</span>
</a>
);
}
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@
"!apps/web/dist/assets/binderDialogs-*.js",
"!apps/web/dist/assets/saveRules-*.js"
],
"limit": "468 kB",
"limit": "469 kB",
"gzip": true
},
{
Expand Down
Loading