RRS First version
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
node_modules/
|
||||
dist/
|
||||
test-results/
|
||||
playwright-report/
|
||||
.DS_Store
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="theme-color" content="#f5f3ee" />
|
||||
<meta
|
||||
name="description"
|
||||
content="Radhe Radhe Synthetics — greige knitted fabrics engineered for diverse apparel applications. Consistency at scale. Continuous development."
|
||||
/>
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<title>RRS — Radhe Radhe Synthetics</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.jsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
Generated
+1943
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "rrs",
|
||||
"private": true,
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite --host 0.0.0.0",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview --host 0.0.0.0",
|
||||
"test": "playwright test"
|
||||
},
|
||||
"dependencies": {
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"react-router-dom": "^7.6.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.52.0",
|
||||
"@vitejs/plugin-react": "^4.5.0",
|
||||
"vite": "^6.3.5"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { defineConfig } from "@playwright/test";
|
||||
|
||||
export default defineConfig({
|
||||
testDir: "./tests",
|
||||
fullyParallel: true,
|
||||
workers: 2,
|
||||
reporter: "list",
|
||||
use: { baseURL: "http://127.0.0.1:5173", screenshot: "only-on-failure" },
|
||||
webServer: {
|
||||
command: "npm run dev -- --port 5173 --strictPort",
|
||||
url: "http://127.0.0.1:5173",
|
||||
reuseExistingServer: !process.env.CI,
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><rect width="64" height="64" rx="14" fill="#282a23"/><path d="M18 47V17h14c15 0 15 20 0 20H18m14 0 13 10" fill="none" stroke="#f5f3ee" stroke-width="5"/></svg>
|
||||
|
After Width: | Height: | Size: 220 B |
@@ -0,0 +1,93 @@
|
||||
Copyright 2014 The DM Sans Project Authors (https://github.com/googlefonts/dm-fonts)
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
https://scripts.sil.org/OFL
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
Binary file not shown.
@@ -0,0 +1,93 @@
|
||||
Copyright 2022 The Instrument Serif Project Authors (https://github.com/Instrument/instrument-serif)
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
https://scripts.sil.org/OFL
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 837 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 720 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 499 KiB |
@@ -0,0 +1,48 @@
|
||||
"""Create original illustrative knit textures, not photographs of RRS products.
|
||||
Requires Pillow and NumPy. Generated assets are committed; runtime needs neither.
|
||||
"""
|
||||
from pathlib import Path
|
||||
import math
|
||||
import random
|
||||
import numpy as np
|
||||
from PIL import Image, ImageDraw, ImageFilter
|
||||
|
||||
OUTPUT = Path(__file__).resolve().parents[1] / 'public' / 'images'
|
||||
OUTPUT.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
|
||||
def texture(name, spacing, base, seed):
|
||||
rng = random.Random(seed)
|
||||
width, height = 1600, 1200
|
||||
image = Image.new('RGB', (width, height), tuple(round(c * .7) for c in base))
|
||||
draw = ImageDraw.Draw(image)
|
||||
for row in range(-2, round(height / (spacing * .72)) + 3):
|
||||
for column in range(-2, width // spacing + 3):
|
||||
x = column * spacing + math.sin(row * .16) * spacing * .16
|
||||
y = row * spacing * .72
|
||||
tone = rng.uniform(.91, 1.05)
|
||||
for side in (-1, 1):
|
||||
points = []
|
||||
for step in range(35):
|
||||
t = step / 34
|
||||
px = x + side * spacing * (.04 + .43 * (1 - t) + .07 * math.sin(t * math.pi))
|
||||
py = y + spacing * (t * .95 - .15)
|
||||
points.append((px, py))
|
||||
draw.line([(px + 3, py + 4) for px, py in points], fill=tuple(round(c * .55) for c in base), width=round(spacing * .36))
|
||||
draw.line(points, fill=tuple(min(255, round(c * tone * .84)) for c in base), width=round(spacing * .32))
|
||||
for strand in range(11):
|
||||
offset = (strand - 5) * spacing * .027
|
||||
fibre = [(px + offset + math.sin(i * .9 + strand) * .7, py) for i, (px, py) in enumerate(points)]
|
||||
light = tone * (.78 + .24 * math.sin(strand * .28))
|
||||
draw.line(fibre, fill=tuple(min(255, round(c * light)) for c in base), width=max(1, spacing // 32))
|
||||
pixels = np.asarray(image).astype(float)
|
||||
yy, xx = np.mgrid[0:height, 0:width]
|
||||
folds = .91 + .14 * np.sin(xx / 210 + yy / 380) + .05 * np.sin(xx / 82 - yy / 230)
|
||||
noise = np.random.default_rng(seed).normal(0, 2.1, (height, width))
|
||||
pixels = np.clip(pixels * folds[:, :, None] + noise[:, :, None], 0, 255).astype('uint8')
|
||||
Image.fromarray(pixels).filter(ImageFilter.GaussianBlur(.3)).save(OUTPUT / name, quality=90, optimize=True)
|
||||
|
||||
|
||||
texture('knit-natural.jpg', 70, (218, 205, 181), 21)
|
||||
texture('knit-fine.jpg', 27, (221, 214, 197), 42)
|
||||
texture('knit-structured.jpg', 108, (180, 170, 147), 63)
|
||||
@@ -0,0 +1,86 @@
|
||||
@font-face {
|
||||
font-family: "Instrument Serif";
|
||||
src: url("/fonts/instrument-serif-regular.ttf") format("truetype");
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
@font-face {
|
||||
font-family: "Instrument Serif";
|
||||
src: url("/fonts/instrument-serif-italic.ttf") format("truetype");
|
||||
font-weight: 400;
|
||||
font-style: italic;
|
||||
font-display: swap;
|
||||
}
|
||||
@font-face {
|
||||
font-family: "DM Sans";
|
||||
src: url("/fonts/dm-sans.woff2") format("woff2");
|
||||
font-weight: 100 1000;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
:root {
|
||||
--paper: #f5f3ee;
|
||||
--ink: #282a23;
|
||||
--muted: #686b61;
|
||||
--line: #d9d9cf;
|
||||
--wash: #ecebe4;
|
||||
--accent: #8f725c;
|
||||
--serif: "Instrument Serif", Georgia, serif;
|
||||
--sans: "DM Sans", Arial, sans-serif;
|
||||
--mono: "Courier New", monospace;
|
||||
--page-gutter: clamp(20px, 4vw, 72px);
|
||||
color-scheme: light;
|
||||
font-family: var(--sans);
|
||||
color: var(--ink);
|
||||
background: var(--paper);
|
||||
font-synthesis: none;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
/* Only box sizing, base document defaults and accessibility are global.
|
||||
Every visual component owns a BEM block in its own stylesheet. */
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
body {
|
||||
margin: 0;
|
||||
min-width: 320px;
|
||||
}
|
||||
button,
|
||||
input,
|
||||
textarea,
|
||||
select {
|
||||
font: inherit;
|
||||
}
|
||||
button,
|
||||
a {
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
:focus-visible:not([tabindex="-1"]) {
|
||||
outline: 2px solid var(--accent);
|
||||
outline-offset: 5px;
|
||||
}
|
||||
::selection {
|
||||
background: #d1d3bf;
|
||||
color: #282a23;
|
||||
}
|
||||
.not-found {
|
||||
padding: 100px 0;
|
||||
}
|
||||
.not-found__label {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--muted);
|
||||
}
|
||||
.not-found__title {
|
||||
font-family: var(--serif);
|
||||
font-size: 72px;
|
||||
font-weight: 400;
|
||||
margin: 20px 0;
|
||||
}
|
||||
.not-found__text {
|
||||
margin-bottom: 35px;
|
||||
color: var(--muted);
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
.capacity-value {
|
||||
display: inline-flex;
|
||||
align-items: baseline;
|
||||
line-height: 1;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.capacity-value__window {
|
||||
display: inline-block;
|
||||
height: 1em;
|
||||
overflow: hidden;
|
||||
}
|
||||
.capacity-value__reel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 4em;
|
||||
}
|
||||
.capacity-value__reel--up {
|
||||
transform: translateY(0);
|
||||
}
|
||||
.capacity-value__reel--down {
|
||||
transform: translateY(-3em);
|
||||
}
|
||||
.capacity-value__step {
|
||||
display: block;
|
||||
height: 1em;
|
||||
flex: 0 0 1em;
|
||||
line-height: 1;
|
||||
}
|
||||
.capacity-value__dash {
|
||||
display: inline-block;
|
||||
}
|
||||
.reveal--visible .capacity-value__reel--up {
|
||||
animation: capacity-roll-up 1.2s cubic-bezier(0.22, 1, 0.36, 1) 180ms both;
|
||||
}
|
||||
.reveal--visible .capacity-value__reel--down {
|
||||
animation: capacity-roll-down 1.2s cubic-bezier(0.22, 1, 0.36, 1) 180ms both;
|
||||
}
|
||||
@keyframes capacity-roll-up {
|
||||
from {
|
||||
transform: translateY(0);
|
||||
}
|
||||
to {
|
||||
transform: translateY(-3em);
|
||||
}
|
||||
}
|
||||
@keyframes capacity-roll-down {
|
||||
from {
|
||||
transform: translateY(-3em);
|
||||
}
|
||||
to {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.reveal--visible .capacity-value__reel--up {
|
||||
animation: none;
|
||||
transform: translateY(-3em);
|
||||
}
|
||||
.reveal--visible .capacity-value__reel--down {
|
||||
animation: none;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import "./CapacityValue.css";
|
||||
|
||||
function Reel({ values, direction }) {
|
||||
return (
|
||||
<span className="capacity-value__window" aria-hidden="true">
|
||||
<span
|
||||
className={`capacity-value__reel capacity-value__reel--${direction}`}
|
||||
>
|
||||
{values.map((value) => (
|
||||
<span className="capacity-value__step" key={value}>
|
||||
{value}
|
||||
</span>
|
||||
))}
|
||||
</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
export default function CapacityValue() {
|
||||
return (
|
||||
<span className="capacity-value" aria-label="60 to 70">
|
||||
<Reel values={["00", "20", "40", "60"]} direction="up" />
|
||||
<span className="capacity-value__dash" aria-hidden="true">
|
||||
–
|
||||
</span>
|
||||
<Reel values={["70", "80", "90", "99"]} direction="down" />
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
.gallery {
|
||||
padding-top: 100px;
|
||||
scroll-margin-top: 30px;
|
||||
}
|
||||
.gallery__intro {
|
||||
margin: 0 0 22px;
|
||||
}
|
||||
.gallery__grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 38px 26px;
|
||||
}
|
||||
.sample {
|
||||
min-width: 0;
|
||||
}
|
||||
.sample__visual {
|
||||
position: relative;
|
||||
aspect-ratio: 1 / 1.12;
|
||||
overflow: hidden;
|
||||
border-radius: 4px;
|
||||
background: var(--wash);
|
||||
}
|
||||
.sample__image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
object-fit: cover;
|
||||
transition: transform 700ms ease;
|
||||
}
|
||||
.sample:hover .sample__image {
|
||||
transform: scale(1.045);
|
||||
}
|
||||
.sample__badge {
|
||||
position: absolute;
|
||||
top: 16px;
|
||||
left: 16px;
|
||||
padding: 7px 10px;
|
||||
background: #f5f3eedb;
|
||||
border-radius: 2px;
|
||||
font-size: 11px;
|
||||
}
|
||||
.sample__heading {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: baseline;
|
||||
gap: 12px;
|
||||
padding-top: 20px;
|
||||
}
|
||||
.sample__name {
|
||||
font-family: var(--serif);
|
||||
font-size: 27px;
|
||||
font-weight: 400;
|
||||
letter-spacing: -0.035em;
|
||||
margin: 0;
|
||||
}
|
||||
.sample__detail {
|
||||
font-size: 11px;
|
||||
color: var(--muted);
|
||||
text-align: right;
|
||||
}
|
||||
.sample__pending {
|
||||
font-size: 11px;
|
||||
color: var(--muted);
|
||||
margin: 9px 0 0;
|
||||
}
|
||||
.gallery__notice {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 14px;
|
||||
margin-top: 32px;
|
||||
padding: 19px 22px;
|
||||
background: var(--wash);
|
||||
border-radius: 4px;
|
||||
color: var(--muted);
|
||||
}
|
||||
.gallery__notice-mark {
|
||||
flex-shrink: 0;
|
||||
width: 17px;
|
||||
height: 17px;
|
||||
display: grid;
|
||||
place-content: center;
|
||||
border: 1px solid currentColor;
|
||||
border-radius: 50%;
|
||||
font-family: var(--serif);
|
||||
font-size: 12px;
|
||||
}
|
||||
.gallery__notice-text {
|
||||
font-size: 11px;
|
||||
line-height: 1.8;
|
||||
margin: 0;
|
||||
}
|
||||
@media (max-width: 900px) {
|
||||
.sample__heading {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
}
|
||||
@media (max-width: 600px) {
|
||||
.gallery {
|
||||
padding-top: 60px;
|
||||
}
|
||||
.gallery__grid {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 30px;
|
||||
}
|
||||
.sample__visual {
|
||||
aspect-ratio: 1.15;
|
||||
}
|
||||
.sample__heading {
|
||||
flex-direction: row;
|
||||
align-items: baseline;
|
||||
}
|
||||
.gallery__notice {
|
||||
padding: 16px;
|
||||
}
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.sample__image {
|
||||
transition: none;
|
||||
}
|
||||
.sample:hover .sample__image {
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
import { developments } from "../data";
|
||||
import { Action, SectionHeading } from "./Primitives";
|
||||
import "./Gallery.css";
|
||||
|
||||
const textures = ["fine", "natural", "structured"];
|
||||
|
||||
export default function Gallery({ preview = false }) {
|
||||
const items = preview ? developments.slice(0, 3) : developments;
|
||||
return (
|
||||
<section className="gallery" id="developments" tabIndex={-1}>
|
||||
<SectionHeading
|
||||
centered
|
||||
label="Selected developments"
|
||||
title={
|
||||
preview ? (
|
||||
<>
|
||||
Structure changes
|
||||
<br />
|
||||
<em>everything.</em>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
From greige
|
||||
<br />
|
||||
<em>to possibility.</em>
|
||||
</>
|
||||
)
|
||||
}
|
||||
>
|
||||
<p className="gallery__intro">
|
||||
{preview
|
||||
? "From subtle surface structures to more expressive knitted textures, different yarn combinations and machine setups produce entirely different fabric characteristics."
|
||||
: "Selected developments are processed separately as reference samples to explore how a construction can translate after dyeing and finishing. RRS commercially manufactures greige fabric only."}
|
||||
</p>
|
||||
{preview && (
|
||||
<Action to="/fabrics#developments" variant="outline">
|
||||
View developments
|
||||
</Action>
|
||||
)}
|
||||
</SectionHeading>
|
||||
<div className="gallery__grid">
|
||||
{items.map((name, index) => (
|
||||
<article className="sample" key={name}>
|
||||
<div className="sample__visual">
|
||||
<img
|
||||
className="sample__image"
|
||||
src={`/images/knit-${textures[index % textures.length]}.jpg`}
|
||||
alt={`Illustrative placeholder for ${name}; RRS sample photograph not yet supplied`}
|
||||
width="1600"
|
||||
height="1200"
|
||||
loading="lazy"
|
||||
/>
|
||||
<span className="sample__badge">Image placeholder</span>
|
||||
</div>
|
||||
<div className="sample__heading">
|
||||
<h3 className="sample__name">{name}</h3>
|
||||
<span className="sample__detail">Development reference</span>
|
||||
</div>
|
||||
<p className="sample__pending">RRS sample photography to follow.</p>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
<aside className="gallery__notice">
|
||||
<span className="gallery__notice-mark" aria-hidden="true">
|
||||
i
|
||||
</span>
|
||||
<div>
|
||||
<p className="gallery__notice-text">
|
||||
Illustrative textures shown, not actual RRS samples. Development
|
||||
names are references, not technical categories.
|
||||
</p>
|
||||
<p className="gallery__notice-text">
|
||||
When supplied, processed images will be labelled{" "}
|
||||
<strong>Processed Development Reference</strong>. Processed samples
|
||||
are for visual reference; RRS commercially manufactures and supplies
|
||||
greige fabric only. Finished appearance may vary depending on the
|
||||
buyer’s processing and finishing method.
|
||||
</p>
|
||||
</div>
|
||||
</aside>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,353 @@
|
||||
.site-main {
|
||||
margin-inline: var(--page-gutter);
|
||||
outline: none;
|
||||
}
|
||||
.reveal {
|
||||
opacity: 0;
|
||||
clip-path: inset(0 0 12% 0);
|
||||
transform: translateY(36px);
|
||||
transition:
|
||||
opacity 700ms ease,
|
||||
clip-path 850ms cubic-bezier(0.22, 1, 0.36, 1),
|
||||
transform 850ms cubic-bezier(0.22, 1, 0.36, 1);
|
||||
will-change: opacity, clip-path, transform;
|
||||
}
|
||||
.reveal--visible {
|
||||
opacity: 1;
|
||||
clip-path: inset(0);
|
||||
transform: translateY(0);
|
||||
will-change: auto;
|
||||
}
|
||||
.skip-link {
|
||||
position: fixed;
|
||||
left: 20px;
|
||||
top: -80px;
|
||||
z-index: 100;
|
||||
padding: 16px 24px;
|
||||
background: var(--ink);
|
||||
color: var(--paper);
|
||||
}
|
||||
.skip-link:focus {
|
||||
top: 10px;
|
||||
}
|
||||
.site-header {
|
||||
border-bottom: 1px solid var(--line);
|
||||
}
|
||||
.site-header__inner {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
min-height: 100px;
|
||||
margin-inline: var(--page-gutter);
|
||||
gap: 30px;
|
||||
}
|
||||
.brand {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
text-decoration: none;
|
||||
color: var(--ink);
|
||||
}
|
||||
.brand__mark {
|
||||
font-family: var(--serif);
|
||||
font-size: 42px;
|
||||
font-weight: 600;
|
||||
line-height: 1;
|
||||
letter-spacing: -0.065em;
|
||||
}
|
||||
.brand__period {
|
||||
color: var(--accent);
|
||||
}
|
||||
.brand__name {
|
||||
border-left: 1px solid var(--line);
|
||||
padding-left: 16px;
|
||||
font-size: 9px;
|
||||
line-height: 1.65;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.12em;
|
||||
}
|
||||
.site-nav {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 34px;
|
||||
}
|
||||
.site-nav__link {
|
||||
position: relative;
|
||||
padding: 14px 0;
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
text-decoration: none;
|
||||
}
|
||||
.site-nav__link--active {
|
||||
color: var(--ink);
|
||||
}
|
||||
.site-nav__link--active::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
bottom: 3px;
|
||||
left: calc(50% - 2px);
|
||||
width: 4px;
|
||||
height: 4px;
|
||||
background: var(--ink);
|
||||
border-radius: 50%;
|
||||
}
|
||||
.site-nav__link:hover {
|
||||
color: var(--ink);
|
||||
}
|
||||
.site-nav__contact {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 30px;
|
||||
margin-left: 45px;
|
||||
padding: 12px 20px;
|
||||
border: 1px solid var(--ink);
|
||||
border-radius: 30px;
|
||||
color: var(--ink);
|
||||
text-decoration: none;
|
||||
font-size: 11px;
|
||||
}
|
||||
.site-nav__contact:hover {
|
||||
background: var(--ink);
|
||||
color: var(--paper);
|
||||
}
|
||||
.site-header__toggle {
|
||||
display: none;
|
||||
}
|
||||
.site-footer {
|
||||
margin-top: 100px;
|
||||
padding: 55px var(--page-gutter) 24px;
|
||||
background: var(--wash);
|
||||
}
|
||||
.site-footer__top {
|
||||
display: grid;
|
||||
grid-template-columns: 1.6fr 0.7fr 1fr;
|
||||
gap: 60px;
|
||||
}
|
||||
.site-footer__name {
|
||||
font-family: var(--serif);
|
||||
font-size: 23px;
|
||||
margin: 0 0 15px;
|
||||
}
|
||||
.site-footer__description {
|
||||
max-width: 310px;
|
||||
font-size: 13px;
|
||||
color: var(--muted);
|
||||
line-height: 1.8;
|
||||
}
|
||||
.site-footer__nav,
|
||||
.site-footer__contact {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
.site-footer__label {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
margin-bottom: 10px;
|
||||
color: var(--muted);
|
||||
}
|
||||
.site-footer__link {
|
||||
display: flex;
|
||||
gap: 14px;
|
||||
align-items: center;
|
||||
color: var(--ink);
|
||||
font-size: 12px;
|
||||
text-decoration: none;
|
||||
}
|
||||
.site-footer__link:hover {
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 4px;
|
||||
}
|
||||
.site-footer__pending {
|
||||
margin: 0;
|
||||
color: var(--muted);
|
||||
font-size: 11px;
|
||||
}
|
||||
.site-footer__bottom {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-end;
|
||||
padding-top: 60px;
|
||||
}
|
||||
.site-footer__wordmark {
|
||||
font-family: var(--serif);
|
||||
font-weight: 600;
|
||||
font-size: clamp(110px, 18vw, 240px);
|
||||
line-height: 0.88;
|
||||
letter-spacing: -0.075em;
|
||||
}
|
||||
.site-footer__signature {
|
||||
margin: 0 0 15px;
|
||||
text-align: right;
|
||||
font-family: var(--serif);
|
||||
font-size: clamp(25px, 3vw, 40px);
|
||||
line-height: 1.15;
|
||||
letter-spacing: -0.03em;
|
||||
}
|
||||
.site-footer__legal {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 20px;
|
||||
margin-top: 38px;
|
||||
padding-top: 23px;
|
||||
border-top: 1px solid var(--line);
|
||||
font-family: var(--mono);
|
||||
font-size: 8px;
|
||||
line-height: 1.6;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.02em;
|
||||
color: var(--muted);
|
||||
}
|
||||
.site-footer__top-link {
|
||||
color: var(--ink);
|
||||
text-decoration: none;
|
||||
}
|
||||
.contact-banner {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-height: 420px;
|
||||
background: var(--ink);
|
||||
color: var(--paper);
|
||||
padding: 64px;
|
||||
border-radius: 6px;
|
||||
margin-top: 95px;
|
||||
}
|
||||
.contact-banner__copy {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
max-width: 650px;
|
||||
}
|
||||
.contact-banner__label {
|
||||
margin: 0 0 24px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #c2c3b8;
|
||||
}
|
||||
.contact-banner__title {
|
||||
margin: 0 0 22px;
|
||||
font-family: var(--serif);
|
||||
font-size: clamp(36px, 4.2vw, 60px);
|
||||
font-weight: 400;
|
||||
line-height: 1.1;
|
||||
letter-spacing: -0.04em;
|
||||
}
|
||||
.contact-banner__text {
|
||||
max-width: 440px;
|
||||
color: #c2c3b8;
|
||||
font-size: 14px;
|
||||
line-height: 1.9;
|
||||
margin: 0 0 30px;
|
||||
}
|
||||
.contact-banner__motif {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
gap: 25px;
|
||||
right: -60px;
|
||||
bottom: -80px;
|
||||
transform: rotate(30deg);
|
||||
}
|
||||
.contact-banner__loop {
|
||||
display: block;
|
||||
width: 100px;
|
||||
height: 430px;
|
||||
border: 1px solid #74776955;
|
||||
border-radius: 50%;
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.reveal,
|
||||
.reveal--visible {
|
||||
opacity: 1;
|
||||
clip-path: none;
|
||||
transform: none;
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
@media (max-width: 950px) {
|
||||
.site-nav {
|
||||
gap: 23px;
|
||||
}
|
||||
.site-nav__contact {
|
||||
margin-left: 12px;
|
||||
}
|
||||
.site-footer__top {
|
||||
gap: 30px;
|
||||
}
|
||||
}
|
||||
@media (max-width: 760px) {
|
||||
.site-header__inner {
|
||||
min-height: 80px;
|
||||
flex-wrap: wrap;
|
||||
gap: 0;
|
||||
}
|
||||
.brand__mark {
|
||||
font-size: 35px;
|
||||
}
|
||||
.brand__name {
|
||||
font-size: 8px;
|
||||
}
|
||||
.site-header__toggle {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 25px;
|
||||
align-items: center;
|
||||
min-height: 44px;
|
||||
padding: 8px 0 8px 15px;
|
||||
border: 0;
|
||||
color: var(--ink);
|
||||
background: transparent;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.site-nav {
|
||||
display: none;
|
||||
width: 100%;
|
||||
padding: 14px 0 24px;
|
||||
}
|
||||
.site-nav--open {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 8px 24px;
|
||||
}
|
||||
.site-nav__link {
|
||||
padding: 15px 4px;
|
||||
}
|
||||
.site-nav__link--active::after {
|
||||
left: auto;
|
||||
right: 15px;
|
||||
bottom: 20px;
|
||||
}
|
||||
.site-nav__contact {
|
||||
grid-column: 1 / -1;
|
||||
margin: 12px 0 0;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.contact-banner {
|
||||
padding: 40px 25px;
|
||||
min-height: 400px;
|
||||
margin-top: 60px;
|
||||
}
|
||||
.site-footer {
|
||||
padding: 40px 20px 24px;
|
||||
margin-top: 65px;
|
||||
}
|
||||
.site-footer__top {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 32px 20px;
|
||||
}
|
||||
.site-footer__intro {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
.site-footer__bottom {
|
||||
padding-top: 45px;
|
||||
gap: 15px;
|
||||
}
|
||||
.site-footer__legal {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.site-footer__signature {
|
||||
font-size: 24px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,213 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { Link, NavLink, Outlet, useLocation } from "react-router-dom";
|
||||
import { company, navigation } from "../data";
|
||||
import { Arrow, WhatsAppAction } from "./Primitives";
|
||||
import "./Layout.css";
|
||||
|
||||
function Brand() {
|
||||
return (
|
||||
<Link className="brand" to="/" aria-label="RRS home">
|
||||
<span className="brand__mark">
|
||||
RRS<span className="brand__period">.</span>
|
||||
</span>
|
||||
<span className="brand__name">
|
||||
Radhe Radhe
|
||||
<br />
|
||||
Synthetics
|
||||
</span>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
function Header() {
|
||||
const [menuOpen, setMenuOpen] = useState(false);
|
||||
const closeMenu = () => setMenuOpen(false);
|
||||
return (
|
||||
<header
|
||||
className="site-header"
|
||||
onKeyDown={(event) => {
|
||||
if (menuOpen && event.key === "Escape") {
|
||||
closeMenu();
|
||||
document.getElementById("menu-toggle")?.focus();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="site-header__inner">
|
||||
<Brand />
|
||||
<button
|
||||
id="menu-toggle"
|
||||
className="site-header__toggle"
|
||||
type="button"
|
||||
aria-controls="primary-navigation"
|
||||
aria-expanded={menuOpen}
|
||||
onClick={() => setMenuOpen((open) => !open)}
|
||||
>
|
||||
{menuOpen ? "Close" : "Menu"}
|
||||
<span aria-hidden="true">{menuOpen ? "−" : "+"}</span>
|
||||
</button>
|
||||
<nav
|
||||
id="primary-navigation"
|
||||
aria-label="Main navigation"
|
||||
className={`site-nav${menuOpen ? " site-nav--open" : ""}`}
|
||||
>
|
||||
{navigation.map(({ to, label }) => (
|
||||
<NavLink
|
||||
key={to}
|
||||
to={to}
|
||||
end={to === "/"}
|
||||
onClick={closeMenu}
|
||||
className={({ isActive }) =>
|
||||
`site-nav__link${isActive ? " site-nav__link--active" : ""}`
|
||||
}
|
||||
>
|
||||
{label}
|
||||
</NavLink>
|
||||
))}
|
||||
<Link
|
||||
className="site-nav__contact"
|
||||
to="/contact#whatsapp"
|
||||
onClick={closeMenu}
|
||||
>
|
||||
Let's talk <Arrow diagonal />
|
||||
</Link>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
||||
function Footer() {
|
||||
return (
|
||||
<footer className="site-footer">
|
||||
<div className="site-footer__top">
|
||||
<div className="site-footer__intro">
|
||||
<p className="site-footer__name">{company.name}</p>
|
||||
<p className="site-footer__description">{company.description}</p>
|
||||
</div>
|
||||
<nav aria-label="Footer navigation" className="site-footer__nav">
|
||||
<span className="site-footer__label">Explore</span>
|
||||
{navigation.map(({ to, label }) => (
|
||||
<Link className="site-footer__link" to={to} key={to}>
|
||||
{label}
|
||||
</Link>
|
||||
))}
|
||||
</nav>
|
||||
<div className="site-footer__contact">
|
||||
<span className="site-footer__label">Start a conversation</span>
|
||||
<Link className="site-footer__link" to="/contact#whatsapp">
|
||||
WhatsApp <Arrow diagonal />
|
||||
</Link>
|
||||
<Link className="site-footer__link" to="/contact#direct-contact">
|
||||
Phone · Email · Address <Arrow diagonal />
|
||||
</Link>
|
||||
<p className="site-footer__pending">
|
||||
Contact details to be supplied.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="site-footer__bottom">
|
||||
<span className="site-footer__wordmark" aria-hidden="true">
|
||||
RRS.
|
||||
</span>
|
||||
<p className="site-footer__signature">
|
||||
Built to develop.
|
||||
<br />
|
||||
<em>Built to repeat.</em>
|
||||
</p>
|
||||
</div>
|
||||
<div className="site-footer__legal">
|
||||
<span>
|
||||
© {new Date().getFullYear()} Radhe Radhe Synthetics Pvt. Ltd.
|
||||
</span>
|
||||
<span>Commercial supply: greige fabric only.</span>
|
||||
<a className="site-footer__top-link" href="#top">
|
||||
Back to top ↑
|
||||
</a>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
|
||||
export function ContactBanner({
|
||||
title = (
|
||||
<>
|
||||
Developing your
|
||||
<br />
|
||||
<em>next construction?</em>
|
||||
</>
|
||||
),
|
||||
text = "Talk to RRS about fabric structure, yarn combinations, bulk requirements or new developments.",
|
||||
}) {
|
||||
return (
|
||||
<section className="contact-banner">
|
||||
<div className="contact-banner__copy">
|
||||
<p className="contact-banner__label">
|
||||
A conversation is where it begins
|
||||
</p>
|
||||
<h2 className="contact-banner__title">{title}</h2>
|
||||
<p className="contact-banner__text">{text}</p>
|
||||
<WhatsAppAction variant="light" />
|
||||
</div>
|
||||
<div className="contact-banner__motif" aria-hidden="true">
|
||||
<span className="contact-banner__loop" />
|
||||
<span className="contact-banner__loop" />
|
||||
<span className="contact-banner__loop" />
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Layout() {
|
||||
const { pathname, hash, key } = useLocation();
|
||||
useEffect(() => {
|
||||
const title =
|
||||
navigation.find(({ to }) => to === pathname)?.label ?? "Page not found";
|
||||
document.title = `${title === "Home" ? "RRS" : `${title} — RRS`} | Radhe Radhe Synthetics`;
|
||||
const target = hash
|
||||
? document.getElementById(hash.slice(1))
|
||||
: document.getElementById("main-content");
|
||||
if (hash && target) target.scrollIntoView({ behavior: "instant" });
|
||||
else window.scrollTo({ top: 0, behavior: "instant" });
|
||||
target?.focus({ preventScroll: true });
|
||||
}, [pathname, hash, key]);
|
||||
|
||||
useEffect(() => {
|
||||
const sections = [...document.querySelectorAll("#main-content > section")];
|
||||
const reducedMotion = window.matchMedia(
|
||||
"(prefers-reduced-motion: reduce)",
|
||||
).matches;
|
||||
|
||||
if (reducedMotion || !("IntersectionObserver" in window)) {
|
||||
sections.forEach((section) => section.classList.add("reveal--visible"));
|
||||
return undefined;
|
||||
}
|
||||
|
||||
sections.forEach((section) => section.classList.add("reveal"));
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
entries.forEach((entry) => {
|
||||
if (!entry.isIntersecting) return;
|
||||
entry.target.classList.add("reveal--visible");
|
||||
observer.unobserve(entry.target);
|
||||
});
|
||||
},
|
||||
{ threshold: 0.08, rootMargin: "0px 0px -8% 0px" },
|
||||
);
|
||||
sections.forEach((section) => observer.observe(section));
|
||||
|
||||
return () => observer.disconnect();
|
||||
}, [pathname, key]);
|
||||
|
||||
return (
|
||||
<div className="site-shell" id="top">
|
||||
<a className="skip-link" href="#main-content">
|
||||
Skip to content
|
||||
</a>
|
||||
<Header key={key} />
|
||||
<main className="site-main" id="main-content" tabIndex={-1}>
|
||||
<Outlet />
|
||||
</main>
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
.arrow {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
.action {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 30px;
|
||||
min-height: 48px;
|
||||
padding: 14px 23px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 100px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
transition:
|
||||
background 180ms ease,
|
||||
color 180ms ease;
|
||||
}
|
||||
.action--dark {
|
||||
background: var(--ink);
|
||||
color: var(--paper);
|
||||
}
|
||||
.action--light {
|
||||
background: var(--paper);
|
||||
color: var(--ink);
|
||||
}
|
||||
.action--outline {
|
||||
background: transparent;
|
||||
color: var(--ink);
|
||||
border-color: var(--line);
|
||||
}
|
||||
.action--ghost {
|
||||
background: transparent;
|
||||
color: var(--paper);
|
||||
border-color: #ffffff60;
|
||||
}
|
||||
.action--dark:hover:not(:disabled) {
|
||||
background: #474b3f;
|
||||
}
|
||||
.action--light:hover {
|
||||
background: #dedbcf;
|
||||
}
|
||||
.action--outline:hover {
|
||||
background: var(--wash);
|
||||
}
|
||||
.action--ghost:hover {
|
||||
background: #ffffff15;
|
||||
}
|
||||
.action:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.55;
|
||||
}
|
||||
.eyebrow {
|
||||
margin: 0 0 18px;
|
||||
font-family: var(--sans);
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
font-weight: 500;
|
||||
}
|
||||
.section-heading {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: space-between;
|
||||
gap: 45px;
|
||||
margin-bottom: 44px;
|
||||
}
|
||||
.section-heading__title {
|
||||
max-width: 680px;
|
||||
}
|
||||
.section-heading__heading {
|
||||
margin: 0;
|
||||
font-family: var(--serif);
|
||||
font-size: clamp(32px, 3.6vw, 50px);
|
||||
line-height: 1.12;
|
||||
font-weight: 400;
|
||||
letter-spacing: -0.045em;
|
||||
}
|
||||
.section-heading__aside {
|
||||
max-width: 360px;
|
||||
color: var(--muted);
|
||||
font-size: 14px;
|
||||
line-height: 1.8;
|
||||
}
|
||||
.section-heading--centered {
|
||||
display: block;
|
||||
text-align: center;
|
||||
}
|
||||
.section-heading--centered .section-heading__title {
|
||||
max-width: none;
|
||||
}
|
||||
.section-heading--centered .section-heading__aside {
|
||||
margin: 24px auto 0;
|
||||
}
|
||||
.textile {
|
||||
position: relative;
|
||||
min-width: 0;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
border-radius: 5px;
|
||||
background: #aba08b;
|
||||
}
|
||||
.textile__image {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 300px;
|
||||
object-fit: cover;
|
||||
transition: transform 700ms ease;
|
||||
}
|
||||
.textile:hover .textile__image {
|
||||
transform: scale(1.035);
|
||||
}
|
||||
.textile__caption {
|
||||
position: absolute;
|
||||
left: 18px;
|
||||
bottom: 16px;
|
||||
right: 18px;
|
||||
width: fit-content;
|
||||
padding: 7px 10px;
|
||||
border-radius: 2px;
|
||||
background: #f5f3eeeb;
|
||||
color: #34352d;
|
||||
font-size: 11px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.page-hero {
|
||||
display: grid;
|
||||
grid-template-columns: 1.15fr 1fr;
|
||||
gap: 70px;
|
||||
padding: 66px 0 74px;
|
||||
}
|
||||
.page-hero__copy {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
padding: 22px 0 0;
|
||||
}
|
||||
.page-hero__title {
|
||||
margin: 6px 0 28px;
|
||||
font-family: var(--serif);
|
||||
font-weight: 400;
|
||||
font-size: clamp(48px, 5.6vw, 78px);
|
||||
letter-spacing: -0.055em;
|
||||
line-height: 1.04;
|
||||
}
|
||||
.page-hero__description {
|
||||
max-width: 470px;
|
||||
color: var(--muted);
|
||||
font-size: 16px;
|
||||
line-height: 1.9;
|
||||
}
|
||||
.page-hero__footnote {
|
||||
display: block;
|
||||
padding-top: 46px;
|
||||
margin-top: auto;
|
||||
font-size: 12px;
|
||||
color: var(--muted);
|
||||
}
|
||||
@media (max-width: 760px) {
|
||||
.section-heading {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
gap: 22px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
.page-hero {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 30px;
|
||||
padding: 35px 0 50px;
|
||||
}
|
||||
.page-hero__footnote {
|
||||
padding-top: 26px;
|
||||
}
|
||||
.page-hero__title {
|
||||
font-size: clamp(44px, 10vw, 70px);
|
||||
}
|
||||
.page-hero__copy {
|
||||
padding: 0;
|
||||
}
|
||||
.textile__image {
|
||||
max-height: 460px;
|
||||
}
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.action,
|
||||
.textile__image {
|
||||
transition: none;
|
||||
}
|
||||
.textile:hover .textile__image {
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
import { Link } from "react-router-dom";
|
||||
import { company, whatsappUrl } from "../data";
|
||||
import "./Primitives.css";
|
||||
|
||||
export function Arrow({ diagonal = false }) {
|
||||
return (
|
||||
<svg
|
||||
className="arrow"
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
d={diagonal ? "M6 18 18 6M6 6h12v12" : "M4 12h15m-6-6 6 6-6 6"}
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function Action({ children, to, variant = "dark" }) {
|
||||
return (
|
||||
<Link className={`action action--${variant}`} to={to}>
|
||||
{children}
|
||||
<Arrow />
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
export function WhatsAppAction({
|
||||
children = "Talk to us on WhatsApp",
|
||||
variant = "dark",
|
||||
direct = false,
|
||||
}) {
|
||||
const url = whatsappUrl(company.whatsapp);
|
||||
const className = `action action--${variant}`;
|
||||
if (url)
|
||||
return (
|
||||
<a className={className} href={url} target="_blank" rel="noreferrer">
|
||||
{children}
|
||||
<Arrow diagonal />
|
||||
</a>
|
||||
);
|
||||
if (direct)
|
||||
return (
|
||||
<button
|
||||
className={className}
|
||||
type="button"
|
||||
disabled
|
||||
aria-describedby="whatsapp-status"
|
||||
>
|
||||
{children}
|
||||
<Arrow diagonal />
|
||||
</button>
|
||||
);
|
||||
return (
|
||||
<Action to="/contact#whatsapp" variant={variant}>
|
||||
{children}
|
||||
</Action>
|
||||
);
|
||||
}
|
||||
|
||||
export function Eyebrow({ children }) {
|
||||
return <p className="eyebrow">{children}</p>;
|
||||
}
|
||||
|
||||
export function SectionHeading({ label, title, children, centered = false }) {
|
||||
return (
|
||||
<header
|
||||
className={`section-heading${centered ? " section-heading--centered" : ""}`}
|
||||
>
|
||||
<div className="section-heading__title">
|
||||
<Eyebrow>{label}</Eyebrow>
|
||||
<h2 className="section-heading__heading">{title}</h2>
|
||||
</div>
|
||||
{children && <div className="section-heading__aside">{children}</div>}
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
||||
export function Textile({
|
||||
kind = "natural",
|
||||
className = "",
|
||||
caption = true,
|
||||
eager = false,
|
||||
}) {
|
||||
return (
|
||||
<figure className={`textile textile--${kind} ${className}`}>
|
||||
<img
|
||||
className="textile__image"
|
||||
src={`/images/knit-${kind}.jpg`}
|
||||
alt="Illustrative knitted texture — not an RRS product photograph"
|
||||
width="1600"
|
||||
height="1200"
|
||||
loading={eager ? "eager" : "lazy"}
|
||||
/>
|
||||
{caption && (
|
||||
<figcaption className="textile__caption">
|
||||
Illustrative texture · RRS photography to follow
|
||||
</figcaption>
|
||||
)}
|
||||
</figure>
|
||||
);
|
||||
}
|
||||
|
||||
export function PageHero({ label, title, children, texture = "natural" }) {
|
||||
return (
|
||||
<section className="page-hero">
|
||||
<div className="page-hero__copy">
|
||||
<Eyebrow>{label}</Eyebrow>
|
||||
<h1 className="page-hero__title">{title}</h1>
|
||||
<div className="page-hero__description">{children}</div>
|
||||
<span className="page-hero__footnote">
|
||||
Radhe Radhe Synthetics · Greige knitted fabrics
|
||||
</span>
|
||||
</div>
|
||||
<Textile kind={texture} eager />
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
.stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 32px;
|
||||
border-block: 1px solid var(--line);
|
||||
padding: 42px 0;
|
||||
margin-top: 78px;
|
||||
}
|
||||
.stats__item {
|
||||
min-width: 0;
|
||||
}
|
||||
.stats__label {
|
||||
margin: 0 0 26px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
line-height: 1.6;
|
||||
color: var(--muted);
|
||||
}
|
||||
.stats__value {
|
||||
font-family: var(--serif);
|
||||
font-size: clamp(38px, 4vw, 54px);
|
||||
letter-spacing: -0.035em;
|
||||
line-height: 1.1;
|
||||
margin: 0 0 10px;
|
||||
}
|
||||
.stats__unit {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
line-height: 1.6;
|
||||
margin: 0 0 16px;
|
||||
}
|
||||
.stats__text {
|
||||
max-width: 230px;
|
||||
font-size: 13px;
|
||||
color: var(--muted);
|
||||
line-height: 1.8;
|
||||
margin: 0;
|
||||
}
|
||||
@media (max-width: 850px) {
|
||||
.stats {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 36px 24px;
|
||||
}
|
||||
}
|
||||
@media (max-width: 480px) {
|
||||
.stats {
|
||||
margin-top: 45px;
|
||||
padding: 30px 0;
|
||||
}
|
||||
.stats__value {
|
||||
font-size: 36px;
|
||||
}
|
||||
.stats__label {
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import CapacityValue from "./CapacityValue";
|
||||
import "./Stats.css";
|
||||
|
||||
const facts = [
|
||||
{
|
||||
label: "Production capacity",
|
||||
value: <CapacityValue />,
|
||||
unit: "Tonnes / month",
|
||||
text: "Approximate monthly manufacturing capacity.",
|
||||
},
|
||||
{
|
||||
label: "Our specialisation",
|
||||
value: "Greige",
|
||||
unit: "Knitted fabrics only",
|
||||
text: "The starting point for a world of possibilities.",
|
||||
},
|
||||
{
|
||||
label: "The capability",
|
||||
value: "Circular",
|
||||
unit: "Knitting · multiple constructions",
|
||||
text: "Different structures. Different configurations.",
|
||||
},
|
||||
{
|
||||
label: "The approach",
|
||||
value: "Continuous",
|
||||
unit: "Fabric development",
|
||||
text: "Exploring yarns, blends and new constructions.",
|
||||
},
|
||||
];
|
||||
|
||||
export default function Stats() {
|
||||
return (
|
||||
<section className="stats" aria-label="RRS at a glance">
|
||||
{facts.map(({ label, value, unit, text }) => (
|
||||
<div className="stats__item" key={label}>
|
||||
<p className="stats__label">{label}</p>
|
||||
<p className="stats__value">{value}</p>
|
||||
<p className="stats__unit">{unit}</p>
|
||||
<p className="stats__text">{text}</p>
|
||||
</div>
|
||||
))}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
export const company = Object.freeze({
|
||||
name: "Radhe Radhe Synthetics Pvt. Ltd.",
|
||||
description:
|
||||
"Greige knitted fabrics engineered for diverse apparel applications.",
|
||||
whatsapp: "", // Supply the business number in international digits, without + or spaces.
|
||||
phone: "",
|
||||
email: "",
|
||||
address: "",
|
||||
mapsUrl: "",
|
||||
});
|
||||
|
||||
export const navigation = [
|
||||
{ to: "/", label: "Home" },
|
||||
{ to: "/fabrics", label: "Fabrics" },
|
||||
{ to: "/about", label: "About" },
|
||||
{ to: "/contact", label: "Contact" },
|
||||
];
|
||||
|
||||
export const capabilities = [
|
||||
{
|
||||
title: "Develop",
|
||||
subtitle: "Continuous fabric development",
|
||||
text: "Different structures call for different yarns, blends and machine configurations. RRS continually develops new knitted constructions for evolving apparel requirements.",
|
||||
},
|
||||
{
|
||||
title: "Repeat",
|
||||
subtitle: "Consistency in bulk production",
|
||||
text: "A successful development must remain dependable when production scales. RRS focuses on maintaining repeatability and quality across bulk manufacturing.",
|
||||
},
|
||||
{
|
||||
title: "Manufacture",
|
||||
subtitle: "Extensive circular-knitting capability",
|
||||
text: "A broad range of circular-knitting configurations allows RRS to work across different structures, materials and fabric characteristics.",
|
||||
},
|
||||
];
|
||||
|
||||
export const applications = [
|
||||
{
|
||||
title: "Shirting constructions",
|
||||
text: "Greige knitted fabrics developed for shirt and apparel applications.",
|
||||
},
|
||||
{
|
||||
title: "Textured knits",
|
||||
text: "Knitted constructions where yarn and machine configuration are used to create distinctive surface character.",
|
||||
},
|
||||
{
|
||||
title: "Structured knits",
|
||||
text: "More pronounced knitted structures developed using different yarn combinations and knitting setups.",
|
||||
},
|
||||
{
|
||||
title: "Developed constructions",
|
||||
text: "Fabric developments created around specific visual, structural or material requirements.",
|
||||
},
|
||||
];
|
||||
|
||||
export const yarns = [
|
||||
"Polyester",
|
||||
"Cotton",
|
||||
"Rayon",
|
||||
"Stretch yarns",
|
||||
"Lycra / elastane combinations",
|
||||
"Specialty yarn combinations",
|
||||
"Blended yarn constructions",
|
||||
"Other yarns selected according to fabric requirements",
|
||||
];
|
||||
|
||||
// Development names are references, not technical categories or specifications.
|
||||
export const developments = [
|
||||
"Micro Break",
|
||||
"Luxe",
|
||||
"Jerry",
|
||||
"Killer",
|
||||
"Joker",
|
||||
"Ice Cool",
|
||||
"Maple",
|
||||
];
|
||||
|
||||
export function whatsappUrl(number) {
|
||||
return /^[1-9]\d{7,14}$/.test(number)
|
||||
? `https://wa.me/${number}?text=${encodeURIComponent("Hello RRS, I'd like to discuss a fabric requirement.")}`
|
||||
: null;
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import React from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { BrowserRouter, Route, Routes } from "react-router-dom";
|
||||
import Layout from "./components/Layout";
|
||||
import { Action } from "./components/Primitives";
|
||||
import Home from "./pages/Home";
|
||||
import Fabrics from "./pages/Fabrics";
|
||||
import About from "./pages/About";
|
||||
import Contact from "./pages/Contact";
|
||||
import "./base.css";
|
||||
|
||||
function NotFound() {
|
||||
return (
|
||||
<section className="not-found">
|
||||
<p className="not-found__label">404 / Page not found</p>
|
||||
<h1 className="not-found__title">A loose thread.</h1>
|
||||
<p className="not-found__text">
|
||||
This page doesn’t exist. Let’s return to the beginning.
|
||||
</p>
|
||||
<Action to="/">Back to home</Action>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
createRoot(document.getElementById("root")).render(
|
||||
<React.StrictMode>
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
<Route element={<Layout />}>
|
||||
<Route index element={<Home />} />
|
||||
<Route path="fabrics" element={<Fabrics />} />
|
||||
<Route path="about" element={<About />} />
|
||||
<Route path="contact" element={<Contact />} />
|
||||
<Route path="*" element={<NotFound />} />
|
||||
</Route>
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
</React.StrictMode>,
|
||||
);
|
||||
@@ -0,0 +1,263 @@
|
||||
.philosophy {
|
||||
border-top: 1px solid var(--line);
|
||||
padding-top: 70px;
|
||||
}
|
||||
.philosophy__grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
border-block: 1px solid var(--line);
|
||||
padding-block: 48px;
|
||||
}
|
||||
.philosophy__item {
|
||||
padding: 10px clamp(24px, 4vw, 72px);
|
||||
}
|
||||
.philosophy__item:first-child {
|
||||
text-align: right;
|
||||
}
|
||||
.philosophy__item + .philosophy__item {
|
||||
border-left: 1px solid var(--line);
|
||||
}
|
||||
.philosophy__title {
|
||||
margin: 0 0 18px;
|
||||
font-family: var(--serif);
|
||||
font-weight: 400;
|
||||
font-size: clamp(40px, 4vw, 58px);
|
||||
letter-spacing: -0.04em;
|
||||
}
|
||||
.philosophy__text {
|
||||
max-width: 390px;
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
line-height: 1.9;
|
||||
color: var(--muted);
|
||||
}
|
||||
.philosophy__item:first-child .philosophy__text {
|
||||
margin-left: auto;
|
||||
}
|
||||
.leadership {
|
||||
padding-top: 100px;
|
||||
}
|
||||
.leadership__grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 28px;
|
||||
}
|
||||
.owner {
|
||||
margin: 0;
|
||||
}
|
||||
.owner__portrait {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
position: relative;
|
||||
min-height: 370px;
|
||||
padding: 24px;
|
||||
background: #e5e2d9;
|
||||
color: #7e8271;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 4px;
|
||||
}
|
||||
.owner__image-label {
|
||||
align-self: flex-start;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: var(--muted);
|
||||
}
|
||||
.owner__silhouette {
|
||||
width: 185px;
|
||||
height: 220px;
|
||||
}
|
||||
.owner__pending {
|
||||
font-size: 11px;
|
||||
text-align: center;
|
||||
color: var(--muted);
|
||||
}
|
||||
.owner__caption {
|
||||
padding: 22px 0 0;
|
||||
}
|
||||
.owner__identity {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 20px;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.owner__name {
|
||||
margin: 0;
|
||||
font-family: var(--serif);
|
||||
font-weight: 400;
|
||||
font-size: 26px;
|
||||
}
|
||||
.owner__role {
|
||||
color: var(--muted);
|
||||
font-size: 11px;
|
||||
}
|
||||
.owner__bio {
|
||||
max-width: 370px;
|
||||
margin: 13px 0 0;
|
||||
font-size: 13px;
|
||||
color: var(--muted);
|
||||
line-height: 1.9;
|
||||
}
|
||||
.approach {
|
||||
padding-top: 100px;
|
||||
}
|
||||
.approach__grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 35px;
|
||||
}
|
||||
.approach__step {
|
||||
border-top: 1px solid var(--ink);
|
||||
padding-top: 20px;
|
||||
}
|
||||
.approach__title {
|
||||
margin: 8px 0 18px;
|
||||
font-family: var(--serif);
|
||||
font-size: 36px;
|
||||
font-weight: 400;
|
||||
letter-spacing: -0.04em;
|
||||
}
|
||||
.approach__text {
|
||||
font-size: 13px;
|
||||
line-height: 1.9;
|
||||
color: var(--muted);
|
||||
margin: 0;
|
||||
}
|
||||
.location {
|
||||
padding-top: 100px;
|
||||
}
|
||||
.location__grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1.4fr 1fr;
|
||||
gap: 50px;
|
||||
}
|
||||
.location__map {
|
||||
position: relative;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
min-height: 360px;
|
||||
border: 1px solid #d2d5c7;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
background-color: #e4e6dc;
|
||||
background-image:
|
||||
linear-gradient(#75866b16 1px, transparent 1px),
|
||||
linear-gradient(90deg, #75866b16 1px, transparent 1px);
|
||||
background-size: 40px 40px;
|
||||
}
|
||||
.location__map-label {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
left: 20px;
|
||||
right: 20px;
|
||||
font-size: 11px;
|
||||
line-height: 1.6;
|
||||
color: var(--muted);
|
||||
}
|
||||
.location__pin {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
border: 1px solid var(--ink);
|
||||
padding: 14px 21px;
|
||||
background: var(--paper);
|
||||
border-radius: 50px;
|
||||
font-family: var(--serif);
|
||||
font-size: 24px;
|
||||
}
|
||||
.location__pin-dot {
|
||||
width: 9px;
|
||||
height: 9px;
|
||||
background: var(--accent);
|
||||
border-radius: 50%;
|
||||
outline: 1px solid #8f725c50;
|
||||
outline-offset: 5px;
|
||||
margin-right: 4px;
|
||||
}
|
||||
.location__map-caption {
|
||||
position: absolute;
|
||||
bottom: 22px;
|
||||
font-family: var(--serif);
|
||||
font-style: italic;
|
||||
font-size: 18px;
|
||||
color: var(--muted);
|
||||
}
|
||||
.location__details {
|
||||
align-self: center;
|
||||
}
|
||||
.location__name {
|
||||
max-width: 360px;
|
||||
margin: 0 0 25px;
|
||||
font-family: var(--serif);
|
||||
font-size: 31px;
|
||||
font-weight: 400;
|
||||
line-height: 1.2;
|
||||
letter-spacing: -0.025em;
|
||||
}
|
||||
.location__address {
|
||||
font-size: 14px;
|
||||
line-height: 1.9;
|
||||
color: var(--muted);
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
.location__button {
|
||||
display: inline-block;
|
||||
padding: 15px 20px;
|
||||
border: 1px solid var(--ink);
|
||||
background: var(--ink);
|
||||
color: var(--paper);
|
||||
border-radius: 30px;
|
||||
font-size: 11px;
|
||||
text-decoration: none;
|
||||
}
|
||||
.location__button:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.location__status {
|
||||
font-size: 11px;
|
||||
color: var(--muted);
|
||||
line-height: 1.8;
|
||||
}
|
||||
@media (max-width: 650px) {
|
||||
.philosophy {
|
||||
padding-top: 45px;
|
||||
}
|
||||
.philosophy__grid,
|
||||
.leadership__grid,
|
||||
.approach__grid,
|
||||
.location__grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.philosophy__grid {
|
||||
padding-block: 0;
|
||||
}
|
||||
.philosophy__item {
|
||||
padding: 32px 0;
|
||||
}
|
||||
.philosophy__item:first-child {
|
||||
text-align: left;
|
||||
}
|
||||
.philosophy__item:first-child .philosophy__text {
|
||||
margin-left: 0;
|
||||
}
|
||||
.philosophy__item + .philosophy__item {
|
||||
border-top: 1px solid var(--line);
|
||||
border-left: 0;
|
||||
}
|
||||
.leadership,
|
||||
.approach,
|
||||
.location {
|
||||
padding-top: 60px;
|
||||
}
|
||||
.leadership__grid {
|
||||
gap: 35px;
|
||||
}
|
||||
.owner__portrait {
|
||||
min-height: 330px;
|
||||
}
|
||||
.location__grid {
|
||||
gap: 30px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,232 @@
|
||||
import { company } from "../data";
|
||||
import { Eyebrow, PageHero, SectionHeading } from "../components/Primitives";
|
||||
import { ContactBanner } from "../components/Layout";
|
||||
import "./About.css";
|
||||
|
||||
const principles = [
|
||||
{
|
||||
title: "Built to repeat.",
|
||||
text: "A fabric developed successfully should remain dependable when manufactured at scale.",
|
||||
},
|
||||
{
|
||||
title: "Built to develop.",
|
||||
text: "New yarn combinations, structures and constructions create new possibilities for apparel.",
|
||||
},
|
||||
];
|
||||
const process = [
|
||||
{
|
||||
title: "Develop.",
|
||||
text: "A construction begins with the relationship between yarn, machine and structure, with the eventual application in mind.",
|
||||
},
|
||||
{
|
||||
title: "Test.",
|
||||
text: "Selected constructions are evaluated through processed reference samples to explore their possibilities.",
|
||||
},
|
||||
{
|
||||
title: "Repeat.",
|
||||
text: "Successful developments are reproduced consistently in commercial greige production.",
|
||||
},
|
||||
];
|
||||
|
||||
function PortraitPlaceholder({ number }) {
|
||||
return (
|
||||
<figure className="owner">
|
||||
<div
|
||||
className="owner__portrait"
|
||||
role="img"
|
||||
aria-label={`Owner ${number} portrait placeholder; professional photograph to be supplied`}
|
||||
>
|
||||
<span className="owner__image-label">Portrait placeholder</span>
|
||||
<svg
|
||||
className="owner__silhouette"
|
||||
viewBox="0 0 200 230"
|
||||
fill="none"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<circle
|
||||
cx="100"
|
||||
cy="76"
|
||||
r="35"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1"
|
||||
/>
|
||||
<path
|
||||
d="M28 218v-29c0-40 32-72 72-72s72 32 72 72v29"
|
||||
stroke="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M8 8h26M8 8v26m184-26h-26m26 0v26M8 222h26M8 222v-26m184 26h-26m26 0v-26"
|
||||
stroke="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
<span className="owner__pending">
|
||||
Professional photograph to follow
|
||||
</span>
|
||||
</div>
|
||||
<figcaption className="owner__caption">
|
||||
<div className="owner__identity">
|
||||
<h3 className="owner__name">[Owner Name]</h3>
|
||||
<span className="owner__role">[Director / Role]</span>
|
||||
</div>
|
||||
<p className="owner__bio">
|
||||
Biography to be supplied. This profile will introduce the person
|
||||
behind the fabric.
|
||||
</p>
|
||||
</figcaption>
|
||||
</figure>
|
||||
);
|
||||
}
|
||||
|
||||
export default function About() {
|
||||
return (
|
||||
<>
|
||||
<PageHero
|
||||
label="About RRS"
|
||||
title={
|
||||
<>
|
||||
Built around
|
||||
<br />
|
||||
<em>the fabric.</em>
|
||||
</>
|
||||
}
|
||||
>
|
||||
{" "}
|
||||
<p>
|
||||
Radhe Radhe Synthetics is a specialist manufacturer of greige knitted
|
||||
fabrics with a focus on consistent bulk production and continuous
|
||||
fabric development.
|
||||
</p>
|
||||
</PageHero>
|
||||
<section className="philosophy">
|
||||
<SectionHeading
|
||||
centered
|
||||
label="Our philosophy"
|
||||
title={
|
||||
<>
|
||||
Consistency
|
||||
<br />
|
||||
<em>and curiosity.</em>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<p>
|
||||
Two ideas guide the way RRS works. Together, they define how we
|
||||
approach knitting and production.
|
||||
</p>
|
||||
</SectionHeading>
|
||||
<div className="philosophy__grid">
|
||||
{principles.map(({ title, text }) => (
|
||||
<article className="philosophy__item" key={title}>
|
||||
<h3 className="philosophy__title">{title}</h3>
|
||||
<p className="philosophy__text">{text}</p>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
<section className="leadership">
|
||||
<SectionHeading
|
||||
label="Our people"
|
||||
title={
|
||||
<>
|
||||
The people
|
||||
<br />
|
||||
<em>behind RRS.</em>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<p>
|
||||
Meet our owners and directors.
|
||||
<br />
|
||||
Names, roles and biographies will be added when supplied.
|
||||
</p>
|
||||
</SectionHeading>
|
||||
<div className="leadership__grid">
|
||||
<PortraitPlaceholder number={1} />
|
||||
<PortraitPlaceholder number={2} />
|
||||
</div>
|
||||
</section>
|
||||
<section className="approach">
|
||||
<SectionHeading
|
||||
label="Our manufacturing approach"
|
||||
title={
|
||||
<>
|
||||
Develop. Test. <em>Repeat.</em>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<div className="approach__grid">
|
||||
{process.map(({ title, text }) => (
|
||||
<article className="approach__step" key={title}>
|
||||
<h3 className="approach__title">{title}</h3>
|
||||
<p className="approach__text">{text}</p>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
<section className="location">
|
||||
<SectionHeading
|
||||
label="Our location"
|
||||
title={
|
||||
<>
|
||||
Where <em>we work.</em>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<div className="location__grid">
|
||||
<div
|
||||
className="location__map"
|
||||
role="img"
|
||||
aria-label="Map placeholder. Verified factory or office address has not yet been supplied."
|
||||
>
|
||||
<span className="location__map-label">
|
||||
Map placeholder / Not a geographical location
|
||||
</span>
|
||||
<div className="location__pin">
|
||||
<span className="location__pin-dot" />
|
||||
<span>RRS</span>
|
||||
</div>
|
||||
<span className="location__map-caption">
|
||||
A place for our next conversation.
|
||||
</span>
|
||||
</div>
|
||||
<div className="location__details">
|
||||
<Eyebrow>Factory or office</Eyebrow>
|
||||
<h3 className="location__name">{company.name}</h3>
|
||||
<p className="location__address">
|
||||
{company.address || (
|
||||
<>
|
||||
[Full Factory / Office Address]
|
||||
<br />
|
||||
[City, State, PIN, India]
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
{company.mapsUrl ? (
|
||||
<a
|
||||
className="location__button"
|
||||
href={company.mapsUrl}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Open in Google Maps ↗
|
||||
</a>
|
||||
) : (
|
||||
<button
|
||||
className="location__button"
|
||||
type="button"
|
||||
disabled
|
||||
aria-describedby="map-status"
|
||||
>
|
||||
Open in Google Maps ↗
|
||||
</button>
|
||||
)}
|
||||
<p className="location__status" id="map-status">
|
||||
Verified location details to be supplied.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<ContactBanner />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
.whatsapp {
|
||||
display: grid;
|
||||
grid-template-columns: 1.5fr 1fr;
|
||||
align-items: center;
|
||||
gap: 50px;
|
||||
border-top: 2px solid var(--ink);
|
||||
padding: 48px;
|
||||
background: #fbfaf7;
|
||||
border-radius: 5px;
|
||||
scroll-margin-top: 24px;
|
||||
}
|
||||
.whatsapp__title {
|
||||
margin: 0 0 22px;
|
||||
font-family: var(--serif);
|
||||
font-size: clamp(32px, 3.8vw, 48px);
|
||||
font-weight: 400;
|
||||
line-height: 1.13;
|
||||
letter-spacing: -0.04em;
|
||||
}
|
||||
.whatsapp__text {
|
||||
max-width: 460px;
|
||||
font-size: 14px;
|
||||
line-height: 1.9;
|
||||
color: var(--muted);
|
||||
}
|
||||
.whatsapp__message {
|
||||
margin: 28px 0 0;
|
||||
padding: 18px 22px;
|
||||
border-left: 2px solid var(--accent);
|
||||
background: var(--wash);
|
||||
font-family: var(--serif);
|
||||
font-size: 20px;
|
||||
font-style: italic;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.whatsapp__clipboard {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
.whatsapp__copy-button {
|
||||
display: inline-flex;
|
||||
gap: 18px;
|
||||
align-items: center;
|
||||
min-height: 44px;
|
||||
padding: 8px 0;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--ink);
|
||||
font-size: 11px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.whatsapp__copy-button:hover {
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 5px;
|
||||
}
|
||||
.whatsapp__copy-status {
|
||||
font-size: 11px;
|
||||
color: var(--muted);
|
||||
line-height: 1.6;
|
||||
}
|
||||
.whatsapp__action {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
}
|
||||
.whatsapp__icon {
|
||||
display: block;
|
||||
width: 58px;
|
||||
height: 58px;
|
||||
margin-bottom: 28px;
|
||||
}
|
||||
.whatsapp__status {
|
||||
max-width: 270px;
|
||||
margin: 18px 0 0;
|
||||
font-size: 11px;
|
||||
color: var(--muted);
|
||||
line-height: 1.8;
|
||||
}
|
||||
.channels {
|
||||
padding-top: 100px;
|
||||
scroll-margin-top: 25px;
|
||||
}
|
||||
.channels__grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 30px;
|
||||
}
|
||||
.channels__item {
|
||||
min-width: 0;
|
||||
border-top: 1px solid var(--line);
|
||||
padding-top: 20px;
|
||||
}
|
||||
.channels__title {
|
||||
margin: 8px 0 16px;
|
||||
font-size: 13px;
|
||||
color: var(--muted);
|
||||
font-weight: 500;
|
||||
}
|
||||
.channels__value {
|
||||
display: block;
|
||||
font-family: var(--serif);
|
||||
font-size: 24px;
|
||||
color: var(--ink);
|
||||
text-decoration: none;
|
||||
line-height: 1.4;
|
||||
margin: 0;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
.channels__note {
|
||||
font-size: 12px;
|
||||
color: var(--muted);
|
||||
line-height: 1.9;
|
||||
}
|
||||
@media (max-width: 900px) {
|
||||
.whatsapp {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 35px;
|
||||
padding: 32px;
|
||||
}
|
||||
.whatsapp__action {
|
||||
align-items: flex-start;
|
||||
text-align: left;
|
||||
}
|
||||
.channels__grid {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
}
|
||||
@media (max-width: 600px) {
|
||||
.whatsapp {
|
||||
padding: 30px 22px;
|
||||
}
|
||||
.whatsapp__message {
|
||||
padding: 16px;
|
||||
}
|
||||
.channels {
|
||||
padding-top: 60px;
|
||||
}
|
||||
.channels__grid {
|
||||
gap: 25px;
|
||||
}
|
||||
.channels__value {
|
||||
font-size: 21px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
import { useState } from "react";
|
||||
import { company } from "../data";
|
||||
import {
|
||||
Eyebrow,
|
||||
PageHero,
|
||||
SectionHeading,
|
||||
WhatsAppAction,
|
||||
} from "../components/Primitives";
|
||||
import { ContactBanner } from "../components/Layout";
|
||||
import "./Contact.css";
|
||||
|
||||
const message = "Hello RRS, I'd like to discuss a fabric requirement.";
|
||||
|
||||
export default function Contact() {
|
||||
const [copyStatus, setCopyStatus] = useState("");
|
||||
async function copyMessage() {
|
||||
try {
|
||||
await navigator.clipboard.writeText(message);
|
||||
setCopyStatus("Message copied.");
|
||||
} catch {
|
||||
setCopyStatus(
|
||||
"Could not copy automatically. Please select and copy the message above.",
|
||||
);
|
||||
}
|
||||
}
|
||||
const channels = [
|
||||
{
|
||||
title: "WhatsApp",
|
||||
value: company.whatsapp || "[Number]",
|
||||
href: "#whatsapp",
|
||||
note: "Discuss a fabric requirement.",
|
||||
},
|
||||
{
|
||||
title: "Phone",
|
||||
value: company.phone || "[Number]",
|
||||
href: company.phone ? `tel:${company.phone}` : null,
|
||||
note: "A direct conversation with RRS.",
|
||||
},
|
||||
{
|
||||
title: "Email",
|
||||
value: company.email || "[Email Address]",
|
||||
href: company.email ? `mailto:${company.email}` : null,
|
||||
note: "Share your requirement or reference.",
|
||||
},
|
||||
{
|
||||
title: "Address",
|
||||
value: company.address || "[Full Business Address]",
|
||||
href: company.mapsUrl || null,
|
||||
note: "Factory / office location.",
|
||||
},
|
||||
];
|
||||
return (
|
||||
<>
|
||||
<PageHero
|
||||
label="Contact RRS"
|
||||
title={
|
||||
<>
|
||||
Let’s talk
|
||||
<br />
|
||||
<em>fabric.</em>
|
||||
</>
|
||||
}
|
||||
texture="structured"
|
||||
>
|
||||
<p>
|
||||
Whether you’re discussing an existing requirement, bulk production or
|
||||
a new fabric construction, connect directly with RRS.
|
||||
</p>
|
||||
</PageHero>
|
||||
<section className="whatsapp" id="whatsapp" tabIndex={-1}>
|
||||
<div className="whatsapp__copy">
|
||||
<Eyebrow>A direct conversation</Eyebrow>
|
||||
<h2 className="whatsapp__title">
|
||||
Your next construction.
|
||||
<br />
|
||||
<em>Let’s begin here.</em>
|
||||
</h2>
|
||||
<p className="whatsapp__text">
|
||||
Share your fabric requirement, intended application, composition or
|
||||
reference with our team.
|
||||
</p>
|
||||
<blockquote className="whatsapp__message">{message}</blockquote>
|
||||
<div className="whatsapp__clipboard">
|
||||
<button
|
||||
className="whatsapp__copy-button"
|
||||
type="button"
|
||||
onClick={copyMessage}
|
||||
>
|
||||
Copy conversation starter <span aria-hidden="true">↗</span>
|
||||
</button>
|
||||
<span className="whatsapp__copy-status" role="status">
|
||||
{copyStatus}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="whatsapp__action">
|
||||
<svg
|
||||
className="whatsapp__icon"
|
||||
viewBox="0 0 48 48"
|
||||
fill="none"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
d="M40 23a16 16 0 0 1-24 14L7 40l3-9A16 16 0 1 1 40 23Z"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
/>
|
||||
<path
|
||||
d="M17 17c1 7 6 12 13 14l3-4-5-3-2 2c-3-1-5-3-6-6l2-2-3-4-2 3Z"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
<WhatsAppAction direct />
|
||||
<p className="whatsapp__status" id="whatsapp-status">
|
||||
{company.whatsapp
|
||||
? "Opens WhatsApp with a conversation starter."
|
||||
: "Business WhatsApp number to be supplied. Chat will be available once details are confirmed."}
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
<section className="channels" id="direct-contact" tabIndex={-1}>
|
||||
<SectionHeading
|
||||
label="Direct contact"
|
||||
title={
|
||||
<>
|
||||
A line <em>to RRS.</em>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<p>
|
||||
Contact information is awaiting confirmation. No phone numbers,
|
||||
email addresses or locations have been assumed.
|
||||
</p>
|
||||
</SectionHeading>
|
||||
<div className="channels__grid">
|
||||
{channels.map(({ title, value, href, note }) => (
|
||||
<article className="channels__item" key={title}>
|
||||
<h3 className="channels__title">{title}</h3>
|
||||
{href ? (
|
||||
<a className="channels__value" href={href}>
|
||||
{value}
|
||||
</a>
|
||||
) : (
|
||||
<p className="channels__value">{value}</p>
|
||||
)}
|
||||
<p className="channels__note">{note}</p>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
<ContactBanner
|
||||
title={
|
||||
<>
|
||||
Have a construction
|
||||
<br />
|
||||
<em>in mind?</em>
|
||||
</>
|
||||
}
|
||||
text="Share your fabric requirement, intended application, composition or reference with our team and start a conversation about what can be developed."
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,232 @@
|
||||
.applications {
|
||||
border-top: 1px solid var(--line);
|
||||
padding-top: 70px;
|
||||
scroll-margin-top: 30px;
|
||||
}
|
||||
.applications__list {
|
||||
border-top: 1px solid var(--line);
|
||||
}
|
||||
.applications__item {
|
||||
border-bottom: 1px solid var(--line);
|
||||
}
|
||||
.applications__summary {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 30px;
|
||||
padding: 25px 4px;
|
||||
list-style: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
.applications__summary::-webkit-details-marker {
|
||||
display: none;
|
||||
}
|
||||
.applications__title {
|
||||
font-family: var(--serif);
|
||||
font-size: clamp(24px, 2.6vw, 35px);
|
||||
letter-spacing: -0.03em;
|
||||
}
|
||||
.applications__toggle {
|
||||
margin-left: auto;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 50%;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
.applications__toggle::after {
|
||||
content: "+";
|
||||
font-size: 18px;
|
||||
font-weight: 300;
|
||||
}
|
||||
.applications__item[open] .applications__toggle::after {
|
||||
content: "−";
|
||||
}
|
||||
.applications__text {
|
||||
max-width: 680px;
|
||||
margin: 0 40px 28px 4px;
|
||||
color: var(--muted);
|
||||
font-size: 14px;
|
||||
line-height: 1.9;
|
||||
}
|
||||
.materials {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 100px;
|
||||
padding: 100px 0;
|
||||
}
|
||||
.materials__title {
|
||||
font-family: var(--serif);
|
||||
font-size: clamp(34px, 4vw, 52px);
|
||||
line-height: 1.13;
|
||||
letter-spacing: -0.045em;
|
||||
font-weight: 400;
|
||||
margin: 0 0 28px;
|
||||
}
|
||||
.materials__text {
|
||||
font-size: 14px;
|
||||
color: var(--muted);
|
||||
line-height: 1.9;
|
||||
max-width: 400px;
|
||||
}
|
||||
.materials__note {
|
||||
display: block;
|
||||
margin-top: 26px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: var(--muted);
|
||||
line-height: 1.8;
|
||||
}
|
||||
.materials__list {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
.materials__item {
|
||||
padding: 16px 0;
|
||||
border-bottom: 1px solid var(--line);
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
.materials__item:first-child {
|
||||
padding-top: 0;
|
||||
}
|
||||
.knitting {
|
||||
display: grid;
|
||||
grid-template-columns: 1.1fr 1fr;
|
||||
gap: 65px;
|
||||
padding: 60px;
|
||||
border-radius: 5px;
|
||||
background: var(--ink);
|
||||
color: var(--paper);
|
||||
scroll-margin-top: 25px;
|
||||
}
|
||||
.knitting__title {
|
||||
font-family: var(--serif);
|
||||
font-size: clamp(38px, 4.1vw, 56px);
|
||||
line-height: 1.13;
|
||||
letter-spacing: -0.04em;
|
||||
font-weight: 400;
|
||||
margin: 0 0 25px;
|
||||
}
|
||||
.knitting__text {
|
||||
font-size: 13px;
|
||||
color: #c2c3b8;
|
||||
line-height: 1.9;
|
||||
}
|
||||
.knitting__diagram {
|
||||
align-self: center;
|
||||
margin: 0;
|
||||
color: #c1c0a9;
|
||||
}
|
||||
.knitting__drawing {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
.knitting__caption {
|
||||
font-size: 11px;
|
||||
text-align: center;
|
||||
}
|
||||
.knitting__variables {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
margin-top: 28px;
|
||||
padding-top: 18px;
|
||||
border-top: 1px solid #ffffff25;
|
||||
color: #c2c3b8;
|
||||
font-size: 10px;
|
||||
}
|
||||
.production {
|
||||
padding-top: 100px;
|
||||
scroll-margin-top: 25px;
|
||||
}
|
||||
.production__proof {
|
||||
display: grid;
|
||||
grid-template-columns: 1.2fr 1fr;
|
||||
align-items: center;
|
||||
gap: 50px;
|
||||
padding: 42px 50px;
|
||||
background: var(--wash);
|
||||
border-radius: 4px;
|
||||
}
|
||||
.production__number {
|
||||
font-family: var(--serif);
|
||||
font-size: clamp(80px, 10vw, 140px);
|
||||
line-height: 1;
|
||||
letter-spacing: -0.065em;
|
||||
margin: 0;
|
||||
}
|
||||
.production__unit {
|
||||
display: block;
|
||||
margin-top: 18px;
|
||||
font-family: var(--sans);
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
.production__label {
|
||||
display: block;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
line-height: 1.8;
|
||||
}
|
||||
.production__text {
|
||||
margin: 20px 0 28px;
|
||||
max-width: 320px;
|
||||
color: var(--muted);
|
||||
font-size: 14px;
|
||||
line-height: 1.9;
|
||||
}
|
||||
.production__rule {
|
||||
display: block;
|
||||
height: 20px;
|
||||
border-block: 1px solid var(--line);
|
||||
background: repeating-linear-gradient(
|
||||
to right,
|
||||
transparent 0 9px,
|
||||
#7c806b65 9px 10px
|
||||
);
|
||||
}
|
||||
@media (max-width: 850px) {
|
||||
.materials {
|
||||
gap: 45px;
|
||||
}
|
||||
.knitting {
|
||||
padding: 35px;
|
||||
gap: 25px;
|
||||
}
|
||||
.production__proof {
|
||||
padding: 35px;
|
||||
}
|
||||
}
|
||||
@media (max-width: 600px) {
|
||||
.applications {
|
||||
padding-top: 45px;
|
||||
}
|
||||
.applications__summary {
|
||||
gap: 15px;
|
||||
}
|
||||
.applications__text {
|
||||
margin-left: 49px;
|
||||
}
|
||||
.materials {
|
||||
grid-template-columns: 1fr;
|
||||
padding: 60px 0;
|
||||
gap: 30px;
|
||||
}
|
||||
.knitting {
|
||||
grid-template-columns: 1fr;
|
||||
padding: 35px 24px;
|
||||
}
|
||||
.production {
|
||||
padding-top: 60px;
|
||||
}
|
||||
.production__proof {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 32px;
|
||||
padding: 30px 24px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
import { applications, yarns } from "../data";
|
||||
import { Eyebrow, PageHero, SectionHeading } from "../components/Primitives";
|
||||
import { ContactBanner } from "../components/Layout";
|
||||
import CapacityValue from "../components/CapacityValue";
|
||||
import Gallery from "../components/Gallery";
|
||||
import "./Fabrics.css";
|
||||
|
||||
export default function Fabrics() {
|
||||
return (
|
||||
<>
|
||||
<PageHero
|
||||
label="Fabrics & capabilities"
|
||||
title={
|
||||
<>
|
||||
Different yarns.
|
||||
<br />
|
||||
<em>Different structures.</em>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<p>
|
||||
RRS manufactures greige knitted fabrics across diverse yarns, fibres
|
||||
and blends, with constructions developed according to the required
|
||||
texture, appearance and apparel application.
|
||||
</p>
|
||||
</PageHero>
|
||||
<section className="applications" id="applications">
|
||||
<SectionHeading
|
||||
label="Applications"
|
||||
title={
|
||||
<>
|
||||
A construction for
|
||||
<br />
|
||||
<em>the application.</em>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<p>
|
||||
Explore some of the ways yarn and structure come together. These are
|
||||
examples, not the limits of our manufacturing capability.
|
||||
</p>
|
||||
</SectionHeading>
|
||||
<div className="applications__list">
|
||||
{applications.map(({ title, text }, index) => (
|
||||
<details
|
||||
className="applications__item"
|
||||
key={title}
|
||||
open={index === 0}
|
||||
>
|
||||
<summary className="applications__summary">
|
||||
<span className="applications__title">{title}</span>
|
||||
<span className="applications__toggle" aria-hidden="true" />
|
||||
</summary>
|
||||
<p className="applications__text">{text}</p>
|
||||
</details>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
<section className="materials">
|
||||
<div className="materials__intro">
|
||||
<Eyebrow>Materials & yarns</Eyebrow>
|
||||
<h2 className="materials__title">
|
||||
The structure starts
|
||||
<br />
|
||||
<em>with the yarn.</em>
|
||||
</h2>
|
||||
<p className="materials__text">
|
||||
Different fabric designs require different yarn characteristics. RRS
|
||||
manufactures fabrics using a wide variety of yarns and blends,
|
||||
selected around the construction.
|
||||
</p>
|
||||
<span className="materials__note">
|
||||
We manufacture the fabric, not the yarn.
|
||||
</span>
|
||||
</div>
|
||||
<ul className="materials__list">
|
||||
{yarns.map((yarn) => (
|
||||
<li className="materials__item" key={yarn}>
|
||||
{yarn}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
<section className="knitting" id="circular-knitting" tabIndex={-1}>
|
||||
<div className="knitting__copy">
|
||||
<Eyebrow>Circular knitting</Eyebrow>
|
||||
<h2 className="knitting__title">
|
||||
One medium.
|
||||
<br />
|
||||
<em>Many constructions.</em>
|
||||
</h2>
|
||||
<p className="knitting__text">
|
||||
Extensive circular-knitting capability allows RRS to change yarn,
|
||||
composition, structure and machine configuration according to the
|
||||
fabric being developed.
|
||||
</p>
|
||||
<p className="knitting__text">
|
||||
The result is a broad manufacturing vocabulary ranging from
|
||||
understated shirting constructions to highly textured knitted
|
||||
surfaces.
|
||||
</p>
|
||||
</div>
|
||||
<figure className="knitting__diagram">
|
||||
<svg
|
||||
className="knitting__drawing"
|
||||
viewBox="0 0 420 350"
|
||||
fill="none"
|
||||
role="img"
|
||||
aria-label="Conceptual circular knitting illustration, not a machine specification"
|
||||
>
|
||||
<ellipse cx="210" cy="90" rx="148" ry="50" stroke="currentColor" />
|
||||
<ellipse cx="210" cy="90" rx="116" ry="33" stroke="currentColor" />
|
||||
<path
|
||||
d="M62 90v150c0 68 296 68 296 0V90M92 120v145m38-133v147m40-141v148m40-146v149m40-151v148m40-154v147m38-159v145"
|
||||
stroke="currentColor"
|
||||
strokeDasharray="3 4"
|
||||
/>
|
||||
<ellipse cx="210" cy="238" rx="148" ry="50" stroke="currentColor" />
|
||||
<path
|
||||
d="M210 9v38m0 241v45M30 163h30m300 0h30"
|
||||
stroke="currentColor"
|
||||
/>
|
||||
<circle cx="210" cy="90" r="4" fill="currentColor" />
|
||||
</svg>
|
||||
<figcaption className="knitting__caption">
|
||||
Circular knitting / Concept illustration
|
||||
</figcaption>
|
||||
<div className="knitting__variables">
|
||||
<span>Yarn</span>
|
||||
<span>Composition</span>
|
||||
<span>Structure</span>
|
||||
</div>
|
||||
</figure>
|
||||
</section>
|
||||
<section className="production" id="production" tabIndex={-1}>
|
||||
<SectionHeading
|
||||
label="Production"
|
||||
title={
|
||||
<>
|
||||
Developed
|
||||
<br />
|
||||
<em>to scale.</em>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<p>
|
||||
Development is only one part of the process. RRS is built to
|
||||
reproduce successful constructions consistently across commercial
|
||||
quantities.
|
||||
</p>
|
||||
</SectionHeading>
|
||||
<div className="production__proof">
|
||||
<p className="production__number">
|
||||
<CapacityValue />
|
||||
<span className="production__unit">tonnes / month</span>
|
||||
</p>
|
||||
<div className="production__detail">
|
||||
<span className="production__label">
|
||||
Approximate monthly production capacity
|
||||
</span>
|
||||
<p className="production__text">
|
||||
Consistency across bulk orders remains one of our core
|
||||
manufacturing priorities.
|
||||
</p>
|
||||
<span className="production__rule" aria-hidden="true" />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<Gallery />
|
||||
<ContactBanner />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,219 @@
|
||||
.home-hero {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
min-height: 590px;
|
||||
margin-top: 24px;
|
||||
border-radius: 7px;
|
||||
color: var(--paper);
|
||||
background: var(--ink);
|
||||
}
|
||||
.home-hero__image {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
object-position: center 65%;
|
||||
}
|
||||
.home-hero__shade {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: #1419108f;
|
||||
}
|
||||
.home-hero__content {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
padding: 65px 62px 90px;
|
||||
}
|
||||
.home-hero__title {
|
||||
margin: 38px 0 25px;
|
||||
font-family: var(--serif);
|
||||
font-size: clamp(54px, 6.7vw, 94px);
|
||||
font-weight: 400;
|
||||
line-height: 1.02;
|
||||
letter-spacing: -0.055em;
|
||||
}
|
||||
.home-hero__description {
|
||||
max-width: 400px;
|
||||
margin: 0 0 30px;
|
||||
color: #eeece5;
|
||||
font-size: 15px;
|
||||
line-height: 1.8;
|
||||
}
|
||||
.home-hero__actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
}
|
||||
.home-hero__bottom {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 32px;
|
||||
right: 32px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 20px;
|
||||
padding: 20px 0;
|
||||
border-top: 1px solid #ffffff35;
|
||||
font-size: 11px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
.home-hero__tagline {
|
||||
font-size: clamp(14px, 1.2vw, 16px);
|
||||
font-weight: 500;
|
||||
color: #ffffff;
|
||||
letter-spacing: 0.01em;
|
||||
}
|
||||
.home-hero__image-note {
|
||||
color: #e2e2dc;
|
||||
}
|
||||
.home-intro {
|
||||
display: grid;
|
||||
gap: 44px;
|
||||
padding-top: 92px;
|
||||
}
|
||||
.home-intro__heading {
|
||||
text-align: center;
|
||||
}
|
||||
.home-intro__body {
|
||||
width: min(100%, 820px);
|
||||
margin-inline: auto;
|
||||
}
|
||||
.home-intro__title {
|
||||
margin: 0;
|
||||
font-family: var(--serif);
|
||||
font-size: clamp(36px, 4.2vw, 57px);
|
||||
font-weight: 400;
|
||||
line-height: 1.08;
|
||||
letter-spacing: -0.045em;
|
||||
}
|
||||
.home-intro__lead {
|
||||
font-family: var(--serif);
|
||||
font-size: clamp(25px, 2.5vw, 34px);
|
||||
line-height: 1.4;
|
||||
letter-spacing: -0.015em;
|
||||
margin: 0 0 24px;
|
||||
}
|
||||
.home-intro__text {
|
||||
color: var(--muted);
|
||||
font-size: 14px;
|
||||
line-height: 1.9;
|
||||
margin: 0 0 15px;
|
||||
}
|
||||
.home-intro__link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 30px;
|
||||
margin-top: 12px;
|
||||
padding-bottom: 7px;
|
||||
border-bottom: 1px solid var(--ink);
|
||||
color: var(--ink);
|
||||
font-size: 11px;
|
||||
text-decoration: none;
|
||||
}
|
||||
.process {
|
||||
padding-top: 95px;
|
||||
}
|
||||
.process__list {
|
||||
border-top: 1px solid var(--line);
|
||||
}
|
||||
.process__item {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 0.9fr) minmax(0, 1.1fr);
|
||||
align-items: center;
|
||||
gap: clamp(45px, 7vw, 120px);
|
||||
padding: 58px 0;
|
||||
border-bottom: 1px solid var(--line);
|
||||
}
|
||||
.process__item:nth-child(even) .process__visual {
|
||||
order: -1;
|
||||
}
|
||||
.process__copy {
|
||||
max-width: 560px;
|
||||
padding-inline: clamp(0px, 2vw, 34px);
|
||||
}
|
||||
.process__verb {
|
||||
margin: 0 0 24px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--muted);
|
||||
}
|
||||
.process__title {
|
||||
margin: 0 0 22px;
|
||||
font-family: var(--serif);
|
||||
font-size: clamp(35px, 3.3vw, 52px);
|
||||
font-weight: 400;
|
||||
line-height: 1.08;
|
||||
letter-spacing: -0.04em;
|
||||
}
|
||||
.process__text {
|
||||
margin: 0 0 28px;
|
||||
color: var(--muted);
|
||||
font-size: 14px;
|
||||
line-height: 1.9;
|
||||
}
|
||||
.process__link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 24px;
|
||||
padding-bottom: 7px;
|
||||
border-bottom: 1px solid var(--ink);
|
||||
color: var(--ink);
|
||||
font-size: 12px;
|
||||
text-decoration: none;
|
||||
}
|
||||
.process__visual {
|
||||
width: 100%;
|
||||
min-height: 320px;
|
||||
aspect-ratio: 16 / 10;
|
||||
}
|
||||
.process__link:hover {
|
||||
color: var(--accent);
|
||||
}
|
||||
@media (max-width: 760px) {
|
||||
.home-hero {
|
||||
margin-top: 18px;
|
||||
min-height: 560px;
|
||||
}
|
||||
.home-hero__content {
|
||||
padding: 42px 26px 130px;
|
||||
}
|
||||
.home-hero__title {
|
||||
font-size: clamp(44px, 8.9vw, 68px);
|
||||
margin-top: 32px;
|
||||
}
|
||||
.home-hero__bottom {
|
||||
left: 26px;
|
||||
right: 26px;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
padding: 15px 0;
|
||||
}
|
||||
.home-intro {
|
||||
gap: 30px;
|
||||
padding-top: 55px;
|
||||
}
|
||||
.process {
|
||||
padding-top: 60px;
|
||||
}
|
||||
.process__item {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 28px;
|
||||
padding: 42px 0;
|
||||
}
|
||||
.process__visual,
|
||||
.process__item:nth-child(even) .process__visual {
|
||||
order: -1;
|
||||
}
|
||||
.process__copy {
|
||||
max-width: none;
|
||||
padding-inline: 0;
|
||||
}
|
||||
.process__visual {
|
||||
min-height: 280px;
|
||||
aspect-ratio: 4 / 3;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
import { Link } from "react-router-dom";
|
||||
import { capabilities, company } from "../data";
|
||||
import {
|
||||
Action,
|
||||
Arrow,
|
||||
Eyebrow,
|
||||
SectionHeading,
|
||||
Textile,
|
||||
WhatsAppAction,
|
||||
} from "../components/Primitives";
|
||||
import { ContactBanner } from "../components/Layout";
|
||||
import Gallery from "../components/Gallery";
|
||||
import Stats from "../components/Stats";
|
||||
import "./Home.css";
|
||||
|
||||
const processItems = [
|
||||
{
|
||||
...capabilities[0],
|
||||
image: "fine",
|
||||
href: "/fabrics#developments",
|
||||
},
|
||||
{
|
||||
...capabilities[2],
|
||||
image: "structured",
|
||||
href: "/fabrics#circular-knitting",
|
||||
},
|
||||
{
|
||||
...capabilities[1],
|
||||
image: "natural",
|
||||
href: "/fabrics#production",
|
||||
},
|
||||
];
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<>
|
||||
<section className="home-hero">
|
||||
<img
|
||||
className="home-hero__image"
|
||||
src="/images/knit-natural.jpg"
|
||||
alt="Original illustrative macro knit texture, pending RRS photography"
|
||||
width="1600"
|
||||
height="1200"
|
||||
fetchPriority="high"
|
||||
/>
|
||||
<div className="home-hero__shade" />
|
||||
<div className="home-hero__content">
|
||||
<Eyebrow>Circular knitted fabrics · RRS</Eyebrow>
|
||||
<h1 className="home-hero__title">
|
||||
Built to develop.
|
||||
<br />
|
||||
<em>Built to repeat.</em>
|
||||
</h1>
|
||||
<p className="home-hero__description">{company.description}</p>
|
||||
<div className="home-hero__actions">
|
||||
<Action to="/fabrics" variant="light">
|
||||
Explore our fabrics
|
||||
</Action>
|
||||
<WhatsAppAction variant="ghost">Talk to RRS</WhatsAppAction>
|
||||
</div>
|
||||
</div>
|
||||
<div className="home-hero__bottom">
|
||||
<span className="home-hero__tagline">
|
||||
Consistency at scale. Continuous development.
|
||||
</span>
|
||||
<span className="home-hero__image-note">
|
||||
Illustrative texture · Photography to follow
|
||||
</span>
|
||||
</div>
|
||||
</section>
|
||||
<Gallery preview />
|
||||
<section className="home-intro">
|
||||
<div className="home-intro__heading">
|
||||
<Eyebrow>Our fabrics</Eyebrow>
|
||||
<h2 className="home-intro__title">
|
||||
Greige is where
|
||||
<br />
|
||||
<em>it begins.</em>
|
||||
</h2>
|
||||
</div>
|
||||
<div className="home-intro__body">
|
||||
<p className="home-intro__lead">
|
||||
RRS specialises in circular-knitted greige fabrics developed for
|
||||
varied apparel applications.
|
||||
</p>
|
||||
<p className="home-intro__text">
|
||||
Different constructions require different yarns, blends and machine
|
||||
configurations. Our manufacturing capability allows us to develop
|
||||
fabrics around structure, texture, composition and intended use.
|
||||
</p>
|
||||
<p className="home-intro__text">
|
||||
From shirting constructions to textured and structured knits, we
|
||||
work across polyester, cotton, rayon, stretch yarns and varied
|
||||
blends.
|
||||
</p>
|
||||
<Link className="home-intro__link" to="/about">
|
||||
Get to know RRS <Arrow diagonal />
|
||||
</Link>
|
||||
</div>
|
||||
</section>
|
||||
<Stats />
|
||||
<section className="process">
|
||||
<SectionHeading
|
||||
label="How we work"
|
||||
title={
|
||||
<>
|
||||
Behind the fabric:
|
||||
<br />
|
||||
<em>our process.</em>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<p>
|
||||
Development, circular knitting and repeatable bulk production work
|
||||
as one discipline.
|
||||
</p>
|
||||
</SectionHeading>
|
||||
<div className="process__list">
|
||||
{processItems.map(({ title, subtitle, text, image, href }) => (
|
||||
<article className="process__item" key={title}>
|
||||
<div className="process__copy">
|
||||
<p className="process__verb">{title}</p>
|
||||
<h3 className="process__title">{subtitle}</h3>
|
||||
<p className="process__text">{text}</p>
|
||||
<Link className="process__link" to={href}>
|
||||
Explore this capability <Arrow diagonal />
|
||||
</Link>
|
||||
</div>
|
||||
<Textile className="process__visual" kind={image} />
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
<ContactBanner />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,265 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
import { whatsappUrl } from "../src/data.js";
|
||||
|
||||
const pages = [
|
||||
["/", "Built to develop."],
|
||||
["/fabrics", "Different yarns."],
|
||||
["/about", "Built around"],
|
||||
["/contact", "Let’s talk"],
|
||||
];
|
||||
|
||||
for (const width of [1440, 768, 390, 320]) {
|
||||
for (const [path, heading] of pages) {
|
||||
test(`${path} renders without errors at ${width}px`, async ({
|
||||
page,
|
||||
}, testInfo) => {
|
||||
const errors = [];
|
||||
page.on("pageerror", (error) => errors.push(error.message));
|
||||
page.on("console", (message) => {
|
||||
if (message.type() === "error") errors.push(message.text());
|
||||
});
|
||||
await page.setViewportSize({ width, height: 900 });
|
||||
await page.goto(path);
|
||||
await expect(page.getByRole("heading", { level: 1 })).toContainText(
|
||||
heading,
|
||||
);
|
||||
await page.evaluate(() => document.fonts.ready);
|
||||
for (const image of await page.locator("img").all()) {
|
||||
await image.scrollIntoViewIfNeeded();
|
||||
await expect(image).toHaveJSProperty("complete", true);
|
||||
expect(
|
||||
await image.evaluate((element) => element.naturalWidth),
|
||||
).toBeGreaterThan(0);
|
||||
}
|
||||
await page.evaluate(() => window.scrollTo(0, 0));
|
||||
await expect(page.locator("h1")).toHaveCount(1);
|
||||
await expect(page.locator("[style]")).toHaveCount(0);
|
||||
await expect(page.locator("form")).toHaveCount(0);
|
||||
const metrics = await page.evaluate(() => ({
|
||||
overflow: document.documentElement.scrollWidth > window.innerWidth,
|
||||
unloadedImages: [...document.images].filter(
|
||||
(image) => !image.complete || !image.naturalWidth,
|
||||
).length,
|
||||
fontsLoaded:
|
||||
document.fonts.check('16px "Instrument Serif"') &&
|
||||
document.fonts.check('16px "DM Sans"'),
|
||||
contentWidthRatio:
|
||||
document.querySelector("#main-content").getBoundingClientRect()
|
||||
.width / window.innerWidth,
|
||||
}));
|
||||
expect(metrics).toMatchObject({
|
||||
overflow: false,
|
||||
unloadedImages: 0,
|
||||
fontsLoaded: true,
|
||||
});
|
||||
expect(metrics.contentWidthRatio).toBeGreaterThan(0.85);
|
||||
expect(errors).toEqual([]);
|
||||
if (width === 1440 || width === 390) {
|
||||
await page.screenshot({
|
||||
path: testInfo.outputPath("page.png"),
|
||||
fullPage: true,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
test("desktop navigation, anchored CTA and browser back work", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.goto("/");
|
||||
await page
|
||||
.getByRole("link", { name: "Explore our fabrics", exact: true })
|
||||
.click();
|
||||
await expect(page).toHaveURL("/fabrics");
|
||||
await expect(
|
||||
page
|
||||
.getByRole("navigation", { name: "Main navigation" })
|
||||
.getByRole("link", { name: "Fabrics", exact: true }),
|
||||
).toHaveAttribute("aria-current", "page");
|
||||
await page
|
||||
.getByRole("link", { name: "Talk to us on WhatsApp", exact: true })
|
||||
.click();
|
||||
await expect(page).toHaveURL("/contact#whatsapp");
|
||||
await expect(page.locator("#whatsapp")).toBeInViewport();
|
||||
await expect(
|
||||
page.getByRole("button", { name: "Talk to us on WhatsApp", exact: true }),
|
||||
).toBeDisabled();
|
||||
await expect(page.locator("#whatsapp-status")).toContainText(
|
||||
"number to be supplied",
|
||||
);
|
||||
await page.goBack();
|
||||
await expect(page).toHaveURL("/fabrics");
|
||||
await page.goto("/contact#whatsapp");
|
||||
await page
|
||||
.getByRole("link", { name: "Talk to us on WhatsApp", exact: true })
|
||||
.click();
|
||||
await expect(page.locator("#whatsapp")).toBeInViewport();
|
||||
});
|
||||
|
||||
test("mobile navigation closes on selection and Escape", async ({ page }) => {
|
||||
await page.setViewportSize({ width: 390, height: 844 });
|
||||
await page.goto("/");
|
||||
const toggle = page.locator("#menu-toggle");
|
||||
await toggle.click();
|
||||
await expect(toggle).toHaveAttribute("aria-expanded", "true");
|
||||
await page.keyboard.press("Escape");
|
||||
await expect(toggle).toHaveAttribute("aria-expanded", "false");
|
||||
await expect(toggle).toBeFocused();
|
||||
await toggle.click();
|
||||
await page
|
||||
.getByRole("navigation", { name: "Main navigation" })
|
||||
.getByRole("link", { name: "About", exact: true })
|
||||
.click();
|
||||
await expect(page).toHaveURL("/about");
|
||||
await expect(toggle).toHaveAttribute("aria-expanded", "false");
|
||||
await expect(page.locator(".owner")).toHaveCount(2);
|
||||
await toggle.click();
|
||||
await page.getByRole("link", { name: "RRS home", exact: true }).click();
|
||||
await expect(page).toHaveURL("/");
|
||||
await expect(toggle).toHaveAttribute("aria-expanded", "false");
|
||||
});
|
||||
|
||||
test("application disclosures work from the keyboard", async ({ page }) => {
|
||||
await page.goto("/fabrics");
|
||||
const summary = page.locator("summary").filter({ hasText: "Textured knits" });
|
||||
await summary.focus();
|
||||
await page.keyboard.press("Enter");
|
||||
await expect(
|
||||
page.getByText(
|
||||
"Knitted constructions where yarn and machine configuration are used to create distinctive surface character.",
|
||||
),
|
||||
).toBeVisible();
|
||||
await page.keyboard.press("Enter");
|
||||
await expect(
|
||||
page.getByText(
|
||||
"Knitted constructions where yarn and machine configuration are used to create distinctive surface character.",
|
||||
),
|
||||
).not.toBeVisible();
|
||||
await expect(page.locator(".sample")).toHaveCount(7);
|
||||
});
|
||||
|
||||
test("conversation starter copies with accessible feedback", async ({
|
||||
page,
|
||||
context,
|
||||
}) => {
|
||||
await context.grantPermissions(["clipboard-read", "clipboard-write"]);
|
||||
await page.goto("/contact");
|
||||
await page.getByRole("button", { name: "Copy conversation starter" }).click();
|
||||
await expect(page.getByRole("status")).toHaveText("Message copied.");
|
||||
expect(await page.evaluate(() => navigator.clipboard.readText())).toBe(
|
||||
"Hello RRS, I'd like to discuss a fabric requirement.",
|
||||
);
|
||||
});
|
||||
|
||||
test("clipboard denial has a useful fallback", async ({ page }) => {
|
||||
await page.goto("/contact");
|
||||
await page.evaluate(() => {
|
||||
Object.defineProperty(navigator, "clipboard", {
|
||||
value: { writeText: () => Promise.reject(new Error("Denied")) },
|
||||
configurable: true,
|
||||
});
|
||||
});
|
||||
await page.getByRole("button", { name: "Copy conversation starter" }).click();
|
||||
await expect(page.getByRole("status")).toContainText(
|
||||
"Please select and copy",
|
||||
);
|
||||
});
|
||||
|
||||
test("WhatsApp requires a real international number and encodes the message", () => {
|
||||
for (const value of ["", "[Number]", "123", "+12345678901", "not-a-number"])
|
||||
expect(whatsappUrl(value)).toBeNull();
|
||||
const url = new URL(whatsappUrl("12345678901"));
|
||||
expect(url.origin).toBe("https://wa.me");
|
||||
expect(url.searchParams.get("text")).toBe(
|
||||
"Hello RRS, I'd like to discuss a fabric requirement.",
|
||||
);
|
||||
});
|
||||
|
||||
test("every page section unveils once when scrolled into view", async ({
|
||||
page,
|
||||
}) => {
|
||||
for (const [path] of pages) {
|
||||
await page.goto(path);
|
||||
const sections = page.locator("#main-content > section");
|
||||
expect(await sections.count()).toBeGreaterThan(0);
|
||||
for (const section of await sections.all()) {
|
||||
await section.scrollIntoViewIfNeeded();
|
||||
await expect(section).toHaveClass(/reveal--visible/);
|
||||
await expect(section).toHaveCSS("opacity", "1");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test("homepage feature headings are centered semantic h2 elements", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.goto("/");
|
||||
const fabricsHeading = page.getByRole("heading", {
|
||||
level: 2,
|
||||
name: "Greige is where it begins.",
|
||||
});
|
||||
const developmentsHeading = page.getByRole("heading", {
|
||||
level: 2,
|
||||
name: "Structure changes everything.",
|
||||
});
|
||||
await expect(fabricsHeading).toHaveCSS("text-align", "center");
|
||||
await expect(developmentsHeading.locator("..").locator("..")).toHaveClass(
|
||||
/section-heading--centered/,
|
||||
);
|
||||
});
|
||||
|
||||
test("capacity values roll in opposite directions and settle at 60–70", async ({
|
||||
page,
|
||||
}) => {
|
||||
for (const path of ["/", "/fabrics"]) {
|
||||
await page.goto(path);
|
||||
const capacity = page.getByLabel("60 to 70");
|
||||
await expect(capacity).toHaveCount(1);
|
||||
await capacity.scrollIntoViewIfNeeded();
|
||||
await expect
|
||||
.poll(() =>
|
||||
capacity
|
||||
.locator(".capacity-value__reel--up")
|
||||
.evaluate((element) => element.getAnimations().length),
|
||||
)
|
||||
.toBeGreaterThan(0);
|
||||
await expect(capacity.locator(".capacity-value__reel--up")).toHaveCSS(
|
||||
"transform",
|
||||
/matrix/,
|
||||
);
|
||||
await expect(capacity.locator(".capacity-value__reel--down")).toHaveCSS(
|
||||
"transform",
|
||||
/matrix/,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test("section unveiling respects reduced-motion preferences", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.emulateMedia({ reducedMotion: "reduce" });
|
||||
await page.goto("/");
|
||||
const sections = page.locator("#main-content > section");
|
||||
await expect(sections.first()).toHaveClass(/reveal--visible/);
|
||||
for (const section of await sections.all()) {
|
||||
await expect(section).toHaveCSS("opacity", "1");
|
||||
await expect(section).toHaveCSS("transform", "none");
|
||||
}
|
||||
const capacity = page.getByLabel("60 to 70");
|
||||
await capacity.scrollIntoViewIfNeeded();
|
||||
expect(
|
||||
await capacity
|
||||
.locator(".capacity-value__reel--up")
|
||||
.evaluate((element) => element.getAnimations().length),
|
||||
).toBe(0);
|
||||
});
|
||||
|
||||
test("unknown route provides a way home", async ({ page }) => {
|
||||
await page.goto("/missing-page");
|
||||
await expect(page.getByRole("heading", { level: 1 })).toHaveText(
|
||||
"A loose thread.",
|
||||
);
|
||||
await page.getByRole("link", { name: "Back to home", exact: true }).click();
|
||||
await expect(page).toHaveURL("/");
|
||||
});
|
||||
@@ -0,0 +1,12 @@
|
||||
import { defineConfig } from "vite";
|
||||
import react from "@vitejs/plugin-react";
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
server: {
|
||||
watch: {
|
||||
// Avoid caching a temporarily empty module while an editor writes a file.
|
||||
awaitWriteFinish: { stabilityThreshold: 200, pollInterval: 50 },
|
||||
},
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user