/* app-sections-bottom.jsx — Modules, Integrations, UseCases, Proof, CTA form, Footer */
/* ---------------- MODULES ---------------- */
function Modules({ tweaks }) {
const variant = tweaks.moduleStyle; // 'grid' | 'list' | 'compact'
const expandable = variant !== 'compact';
const [open, setOpen] = useState(() => new Set());
const toggle = (i) => {
if (!expandable) return;
setOpen((prev) => {
const n = new Set(prev);
n.has(i) ? n.delete(i) : n.add(i);
return n;
});
};
return (
Everything your quality system needs, and more.
A growing suite of connected modules, all validated, all on Atlassian.
{window.MODULES.map((m, i) => {
const [bg, fg] = window.TINT[m.tint];
const isOpen = open.has(i);
return (
toggle(i)}
role={expandable ? 'button' : undefined}
tabIndex={expandable ? 0 : undefined}
onKeyDown={(e) => {if (expandable && (e.key === 'Enter' || e.key === ' ')) {e.preventDefault();toggle(i);}}}>
{m.tile ? : }
{m.title}
{m.blurb}
{expandable &&
<>
{m.feats.map((f) => {f} )}
{isOpen ? 'Show less' : 'What\u2019s inside'}
>
}
);
})}
Matching process documentation with every app
Matching SOPs, Work Instructions and templates ship with each app — so startups can stand up a compliant quality system from day one, then tailor it as they grow.
);
}
/* ---------------- INTEGRATIONS ---------------- */
function useTravelingHighlight(count, ms) {
const [idx, setIdx] = useState(0);
useEffect(() => {
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return;
const id = setInterval(() => setIdx((i) => (i + 1) % count), ms);
return () => clearInterval(id);
}, [count, ms]);
return idx;
}
function ToolChip({ t, lit, className }) {
return (
{t.q ? : t.l}
{t.nm}
);
}
function IntegrationFlow() {
const flat = [];
window.FLOW.forEach((col, ci) => col.tools.forEach((t, ti) => flat.push({ ci, ti })));
const idx = useTravelingHighlight(flat.length, 950);
const litKey = flat[idx] ? flat[idx].ci + '-' + flat[idx].ti : '';
return (
<>
{window.FLOW.map((col, ci) =>
{col.n} {col.stage}
{col.tools.map((t, ti) =>
)}
{ci < window.FLOW.length - 1 &&
}
)}
Every tool in your workflow feeds compliance evidence to Qity — automatically.
>);
}
function IntegrationTrace({ organic, wide }) {
const ref = useRef(null);
const VB = wide ? window.TRACE_VB_W : window.TRACE_VB;
const TOOLS = wide ? window.TRACE_TOOLS_W : window.TRACE_TOOLS;
const NODES = wide ? window.TRACE_NODES_W : window.TRACE_NODES;
const amp = (organic || 0) / 100 * 48;
const rnd = (s) => {const x = Math.sin(s) * 10000;return x - Math.floor(x);};
const jit = (id, bx, by) => {
if (!amp) return { x: bx, y: by };
const s = [...id].reduce((a, c) => a + c.charCodeAt(0), 0);
return { x: bx + (rnd(s) * 2 - 1) * amp, y: by + (rnd(s * 1.7 + 11) * 2 - 1) * amp };
};
const byId = {};
TOOLS.forEach((t) => byId[t.id] = t);
NODES.forEach((n) => byId[n.id] = { ...n, ...jit(n.id, n.x, n.y) });
const edges = window.TRACE_EDGES;
const SEQ = ['complaint', 'nonconformity', 'capa', 'risk', 'requirement', 'specification', 'test', 'document', 'release', 'change', 'stakeholder', 'bom', 'file'];
const hi = useTravelingHighlight(SEQ.length, 1500);
const activeId = SEQ[hi];
useEffect(() => {
const graph = ref.current;if (!graph) return;
const stage = graph.parentElement;if (!stage) return;
// only when the stage is horizontally scrollable (mobile)
if (stage.scrollWidth - stage.clientWidth < 8) return;
const node = byId[activeId];if (!node) return;
const nodeX = node.x / VB.w * graph.clientWidth;
// a lit node expands its box rightward to reveal the label (~box left -24px, right +220px),
// so frame the whole expanded box — not just the anchor — or the label clips at the edges.
const margin = 16;
const boxL = nodeX - 24;
const boxR = nodeX + 220;
const view = stage.clientWidth;
const maxScroll = stage.scrollWidth - view;
let target;
if (boxR - boxL >= view - margin * 2) {
target = boxL - margin; // box wider than viewport: pin its left edge in view
} else {
target = (boxL + boxR) / 2 - view / 2; // center the full box span
}
target = Math.max(0, Math.min(maxScroll, target));
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) {stage.scrollLeft = target;return;}
const from = stage.scrollLeft;
const dist = target - from;
if (Math.abs(dist) < 2) return;
const dur = 300;let t0 = null,raf = 0;
const ease = (x) => 1 - Math.pow(1 - x, 3);
const step = (ts) => {
if (t0 === null) t0 = ts;
const k = Math.min(1, (ts - t0) / dur);
stage.scrollLeft = from + dist * ease(k);
if (k < 1) raf = requestAnimationFrame(step);
};
raf = requestAnimationFrame(step);
return () => cancelAnimationFrame(raf);
}, [activeId]);
useEffect(() => {
const el = ref.current;if (!el) return;
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) {el.style.setProperty('--p', 1);return;}
el.style.setProperty('--p', 0);
let raf = 0;
const upd = () => {
raf = 0;
const r = el.getBoundingClientRect();
const vh = window.innerHeight || 800;
const start = vh * 0.95,end = vh * 0.42;
const p = Math.max(0, Math.min(1, (start - r.top) / (start - end)));
el.style.setProperty('--p', p.toFixed(3));
};
const onScroll = () => {if (!raf) raf = requestAnimationFrame(upd);};
upd();
window.addEventListener('scroll', onScroll, { passive: true });
window.addEventListener('resize', onScroll);
return () => {window.removeEventListener('scroll', onScroll);window.removeEventListener('resize', onScroll);cancelAnimationFrame(raf);};
}, []);
const pathOf = (a, b) => {
const mx = (a.x + b.x) / 2;
return 'M ' + a.x + ' ' + a.y + ' C ' + mx + ' ' + a.y + ', ' + mx + ' ' + b.y + ', ' + b.x + ' ' + b.y;
};
return (
<>
{edges.map(([f, t], i) => )}
{edges.map(([f, t], i) => {
const grad = ['url(#rovoA)', 'url(#rovoB)', 'url(#rovoC)'][i % 3];
const glow = window.ROVO[i % window.ROVO.length];
const on = f === activeId || t === activeId;
return (
);
})}
{TOOLS.map((t, i) =>
{t.img ?
:
{t.l} }
)}
{NODES.map((n, i) =>
{n.icon ? : }
{n.label}
)}
>);
}
function IntegrationOrbit({ size = 100, labels = false }) {
const ref = useRef(null);
const VB = { w: 820, h: 520 };
const C = { x: 410, y: 306 }; // sphere centered; tools sit just above
const R = 340 * (size / 100); // sphere radius
const TOOLS = window.TRACE_TOOLS;
const NODES = window.ORBIT_NODES;
const N = NODES.length;
const toolSet = useRef(null);
if (!toolSet.current) toolSet.current = new Set(TOOLS.map((t) => t.id));
// tools feed Requirement + Specification directly (lines run from each tool's
// logo in the box); the rest is the traceability spine between work types.
const TOOL_FEED = [['claude', 'requirement'], ['claude', 'standard'], ['altium', 'specification'], ['figma', 'specification'], ['aikido', 'vulnerability'], ['solidworks', 'file'], ['github', 'test'], ['bitbucket', 'test']];
const SPINE = [['complaint', 'nonconformity'], ['nonconformity', 'capa'], ['capa', 'risk'], ['risk', 'requirement'], ['requirement', 'specification'], ['specification', 'test'], ['test', 'change'], ['change', 'batch']];
const ORBIT_EDGES = [...TOOL_FEED, ...SPINE];
// base unit positions on a sphere (fibonacci), decorrelated from array order
// (stride coprime with N) so connected work types spread around the globe.
const base = useRef(null);
if (!base.current || base.current.length !== N) {
const gold = Math.PI * (3 - Math.sqrt(5)),arr = new Array(N);
for (let i = 0; i < N; i++) {
const s = i * 19 % N;
const yy = 1 - s / (N - 1) * 2;
const rad = Math.sqrt(Math.max(0, 1 - yy * yy));
const th = gold * s;
arr[i] = { x: Math.cos(th) * rad, y: yy, z: Math.sin(th) * rad };
}
base.current = arr;
}
const idIndex = {};
NODES.forEach((n, i) => idIndex[n.id] = i);
// scroll reveal --p
useEffect(() => {
const el = ref.current;if (!el) return;
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) {el.style.setProperty('--p', 1);return;}
el.style.setProperty('--p', 0);
let raf = 0;
const upd = () => {
raf = 0;
const r = el.getBoundingClientRect();
const vh = window.innerHeight || 800;
const start = vh * 0.95,end = vh * 0.42;
const p = Math.max(0, Math.min(1, (start - r.top) / (start - end)));
el.style.setProperty('--p', p.toFixed(3));
};
const onScroll = () => {if (!raf) raf = requestAnimationFrame(upd);};
upd();
window.addEventListener('scroll', onScroll, { passive: true });
window.addEventListener('resize', onScroll);
return () => {window.removeEventListener('scroll', onScroll);window.removeEventListener('resize', onScroll);cancelAnimationFrame(raf);};
}, []);
const pathOf = (a, b) => {
const mx = (a.x + b.x) / 2,my = (a.y + b.y) / 2;
const cx = mx + (C.x - mx) * 0.18,cy = my + (C.y - my) * 0.18;
return 'M ' + a.x.toFixed(1) + ' ' + a.y.toFixed(1) + ' Q ' + cx.toFixed(1) + ' ' + cy.toFixed(1) + ', ' + b.x.toFixed(1) + ' ' + b.y.toFixed(1);
};
const wirePath = '';
const nodeEls = useRef([]);
const lastActive = useRef([]);
const baseEls = useRef([]);
const flowEls = useRef([]);
const scaleRef = useRef(1);
const toolXY = useRef({});
// VB units -> screen px + tool-logo positions (so lines start at each tool)
useEffect(() => {
const el = ref.current;if (!el) return;
const measure = () => {
const r = el.getBoundingClientRect();
scaleRef.current = r.width / VB.w;
const logos = el.querySelectorAll('.orbit-tool-logo');
const xy = {};
logos.forEach((lg, i) => {
const id = TOOLS[i] && TOOLS[i].id;if (!id) return;
const lr = lg.getBoundingClientRect();
xy[id] = { x: (lr.left + lr.width / 2 - r.left) / r.width * VB.w, y: (lr.top + lr.height / 2 - r.top) / r.height * VB.h };
});
toolXY.current = xy;
};
measure();
const ro = new ResizeObserver(measure);ro.observe(el);
return () => ro.disconnect();
}, []);
// 3D rotation about a tilted vertical axis; depth drives scale + opacity. Work
// types reveal one-by-one to demonstrate the scale of what the system manages.
useEffect(() => {
const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
const period = 46000;
const phi = 0.34,cphi = Math.cos(phi),sphi = Math.sin(phi);
const tSet = toolSet.current;
const pos = new Array(N);
const lastZ = new Array(N).fill(-1);
const firedState = new Array(N).fill(-1);
const firedNow = new Array(N).fill(0);
const fireT = new Array(ORBIT_EDGES.length).fill(-1e9);
const FIRE_GAP = 1500,FIRE_DUR = 1500;
const srcLast = {}; // source id -> last fire ts, so each source takes turns
let lastFire = -1e9,raf = 0;
const frame = (ts) => {
const theta = ts % period / period * Math.PI * 2;
const ct = Math.cos(theta),st = Math.sin(theta),sc = scaleRef.current;
// all work types are present from the start; depth drives scale + opacity
for (let i = 0; i < N; i++) {
const b = base.current[i];
const x1 = b.x * ct + b.z * st,z1 = -b.x * st + b.z * ct,y1 = b.y;
const y2 = y1 * cphi - z1 * sphi,z2 = y1 * sphi + z1 * cphi;
const front = (z2 + 1) / 2,persp = 1 + 0.16 * z2;
const px = C.x + R * x1 * persp,py = C.y + R * y2 * persp;
pos[i] = { px, py, front, on: true };
const el = nodeEls.current[i];if (!el) continue;
const s = 0.5 + 0.5 * front;
el.style.transform = 'translate(' + (px * sc).toFixed(1) + 'px,' + (py * sc).toFixed(1) + 'px) translate(-50%,-50%) scale(' + s.toFixed(3) + ')';
el.style.opacity = (0.16 + 0.84 * Math.pow(front, 1.3)).toFixed(3);
const zb = 200 + Math.round(front * 40) * 15;
if (zb !== lastZ[i]) {el.style.zIndex = zb;lastZ[i] = zb;}
}
// pathways fire one at a time, taking turns by source: among edges whose
// target faces front, pick the one whose source fired longest ago. This
// gives every tool an equal turn (Claude's two feeds no longer double its
// rate) and avoids the same icon firing twice in a row.
for (let i = 0; i < N; i++) firedNow[i] = 0;
if (ts - lastFire >= FIRE_GAP) {
let best = -1,bestT = Infinity;
for (let cand = 0; cand < ORBIT_EDGES.length; cand++) {
const cf = ORBIT_EDGES[cand][0],cP = pos[idIndex[ORBIT_EDGES[cand][1]]];
const srcFront = tSet.has(cf) ? 1 : pos[idIndex[cf]] ? pos[idIndex[cf]].front : 0;
if (cP && cP.front > 0.58 && srcFront > 0.5) {
const lt = srcLast[cf] == null ? -1e9 : srcLast[cf];
if (lt < bestT) {bestT = lt;best = cand;}
}
}
if (best >= 0) {fireT[best] = ts;lastFire = ts;srcLast[ORBIT_EDGES[best][0]] = ts;}
}
for (let k = 0; k < ORBIT_EDGES.length; k++) {
const baseEl = baseEls.current[k],flowEl = flowEls.current[k];
if (!baseEl || !flowEl) continue;
const age = ts - fireT[k];
if (age < 0 || age > FIRE_DUR) {baseEl.style.opacity = '0';flowEl.style.opacity = '0';continue;}
const f = ORBIT_EDGES[k][0],ti = idIndex[ORBIT_EDGES[k][1]],Pto = pos[ti];
if (!Pto) {baseEl.style.opacity = '0';flowEl.style.opacity = '0';continue;}
let fx, fy;
if (tSet.has(f)) {const o = toolXY.current[f];if (!o) {baseEl.style.opacity = '0';flowEl.style.opacity = '0';continue;}fx = o.x;fy = o.y;} else
{const Pf = pos[idIndex[f]];if (!Pf) {baseEl.style.opacity = '0';flowEl.style.opacity = '0';continue;}fx = Pf.px;fy = Pf.py;}
let env = 1;
if (age < 220) env = age / 220;else
if (age > FIRE_DUR - 450) env = Math.max(0, (FIRE_DUR - age) / 450);
const d = pathOf({ x: fx, y: fy }, { x: Pto.px, y: Pto.py });
baseEl.setAttribute('d', d);flowEl.setAttribute('d', d);
baseEl.style.opacity = (env * 0.26).toFixed(3);flowEl.style.opacity = env.toFixed(3);
if (env > 0.5) firedNow[ti] = 1;
}
for (let i = 0; i < N; i++) {
const el = nodeEls.current[i];if (!el) continue;
const left = pos[i].px > C.x ? 1 : 0,key = firedNow[i] << 1 | left;
if (firedState[i] !== key) {el.classList.toggle('fired', !!firedNow[i]);el.classList.toggle('lbl-left', !!left);firedState[i] = key;}
}
};
if (reduce) {frame(2000);return;}
const tick = (ts) => {frame(ts);raf = requestAnimationFrame(tick);};
raf = requestAnimationFrame(tick);
return () => cancelAnimationFrame(raf);
}, [size, N]);
return (
<>
{ORBIT_EDGES.map((e, i) =>
baseEls.current[i] = el} style={{ opacity: 0 }} pathLength="100" />
)}
{ORBIT_EDGES.map((e, i) =>
flowEls.current[i] = el}
pathLength="100"
style={{ stroke: 'url(#rovoD)', color: window.ROVO[i % window.ROVO.length], opacity: 0, animationDelay: (-(i * 0.31 % 2)).toFixed(2) + 's', animationDuration: (1.3 + i % 5 * 0.2).toFixed(2) + 's' }} />
)}
{Array.from({ length: 2 }).map((_, i) =>
)}
{TOOLS.map((t) =>
{t.img ?
:
{t.l} }
)}
{Array.from({ length: 2 }).map((_, i) =>
)}
{NODES.map((n, i) =>
nodeEls.current[i] = el}>
{n.icon ?
:
}
{n.label}
)}
>);
}
function IntegrationGrid() {
return (
{window.INTG_GROUPS.map((g) =>
{g.cat}
{g.tools.map((t) =>
)}
)}
);
}
function Integrations({ tweaks }) {
const orbit = tweaks && tweaks.intgLayout === 'Jira orbit';
return (
Integrates with design tools . Everything else lives in Jira .
Design tools, source control, CI pipelines, security scanners, all flow into Jira. Source code becomes a source of truth.
);
}
/* ---------------- USE CASES ---------------- */
function Shot({ shot, shotLabel, focal }) {
const pan = focal === 'top right' ? '-19%' : '0%';
return (
);
}
function UCVisual({ kind, shot, shotLabel, focal }) {
if (kind === 'shot') {
return ;
}
if (kind === 'placeholder') {
return (
{shotLabel}
Gantt charts, dashboards & management summaries — add this product screenshot and it drops straight in.
);
}
if (kind === 'ba') {
return (
Before — scattered & manual
Traceability_v7_FINAL.xlsx
SOP-draft (2).docx
audit_evidence.zip
emails / SharePoint
After — generated from Jira
Team works in Jira
Live traceability
Audit-ready
);
}
if (kind === 'flow') {
return (
{[
{ i: 'Ruler', c: 'B', t: 'Figma · Altium · Solidworks', s: 'Design artifacts linked' },
{ i: 'GitBranch', c: 'P', t: 'GitHub · CI/CD', s: 'Commits, builds & tests' },
{ i: 'Sparkles', c: 'G', t: 'Rovo agents', s: 'Specs & test docs drafted' }].
map((r) => {
const [bg, fg] = window.TINT[r.c];
return (
);
})}
);
}
// samd
return (
git commit · sensor-driver
+ feat: ISO 14971 risk control
commit a1f9c2 · pushed to main
Auto-generated in Qity
Design doc
Test report
SBOM
Vuln scan
);
}
function UseCases() {
const [active, setActive] = useState(0);
const [dir, setDir] = useState(1);
const pick = (i) => {setDir(i >= active ? 1 : -1);setActive(i);};
const uc = window.USECASES[active];
return (
Quality for everyone.Zero silos.
{window.USECASES.map((u, i) => pick(i)}>
{u.tab}
)}
{uc.prob &&
{uc.prob}
}
{uc.title}
{uc.body}
);
}
/* ---------------- PROOF ---------------- */
function KitStrip() {
return null;
}
function Proof() {
const items = window.TESTIMONIALS;
const [i, setI] = useState(0);
const many = items.length > 1;
const t = items[i];
const vpRef = useRef(null);
const [m, setM] = useState({ vw: 0, slideW: 0 });
useEffect(() => {
const el = vpRef.current;if (!el) return;
const measure = () => {
const vw = el.clientWidth;
const slideW = Math.min(620, Math.round(vw * (many ? 0.74 : 0.92)));
setM({ vw, slideW });
};
measure();
const ro = new ResizeObserver(measure);
ro.observe(el);
return () => ro.disconnect();
}, [many]);
const GAP = 30;
const go = (d) => setI((p) => (p + d + items.length) % items.length);
const activePos = many ? i + 1 : i;
// wrap-around clones so peeks are cyclic (last shows left of first, first shows right of last)
const display = many ? [items[items.length - 1]].concat(items, [items[0]]) : items;
const offset = m.vw ? (m.vw - m.slideW) / 2 - activePos * (m.slideW + GAP) : 0;
return (
Don't take our word for it
go(-1)} disabled={!many} aria-label="Previous testimonial">
go(1)} disabled={!many} aria-label="Next testimonial">
{display.map((d, p) => {
const logical = many ? p === 0 ? items.length - 1 : p === display.length - 1 ? 0 : p - 1 : p;
const active = p === activePos;
return (
!active && setI(logical)}>
"{d.quote}"
{d.avatar ?
:
{d.name.split(' ').map((w) => w[0]).slice(0, 2).join('')} }
);
})}
);
}
/* ---------------- CTA + DEMO FORM ---------------- */
const DEMO_HREF = 'mailto:info@qity.be?subject=Demo%20request';
function DemoForm() {
return (
Book your demo
Send us a message and our team will get back to you within one business day to schedule a demo and walk through pricing.
Contact us
);
}
/* ---------------- FAQ ---------------- */
function FaqRow({ f, isOpen, onClick }) {
return (
{f.q}
{f.a.split('\n').map((para, k) =>
{para}
)}
);
}
function FAQ({ tweaks }) {
const mode = (tweaks && tweaks.faqStyle) || 'List';
const FAQS = window.FAQS;
const [open, setOpen] = useState(-1); // List: one at a time
const [openSet, setOpenSet] = useState(() => new Set()); // Two columns + Expand all
const [sel, setSel] = useState(0); // Side-by-side selection
const toggleSet = (i) => setOpenSet((s) => {
const n = new Set(s);
n.has(i) ? n.delete(i) : n.add(i);
return n;
});
const allOpen = openSet.size >= FAQS.length;
const toggleAll = () => setOpenSet(allOpen ? new Set() : new Set(FAQS.map((_, i) => i)));
let body;
if (mode === 'Two columns') {
const cols = [[], []];
FAQS.forEach((f, i) => cols[i % 2].push(i)); // interleave for even column heights
body = (
{cols.map((idxs, c) =>
{idxs.map((i) => toggleSet(i)} />)}
)}
);
} else if (mode === 'Expand all') {
body = (
{FAQS.length} questions
{allOpen ? 'Collapse all' : 'Expand all'}
{FAQS.map((f, i) => toggleSet(i)} />)}
);
} else if (mode === 'Side-by-side') {
body = (
{FAQS.map((f, i) =>
setSel(i)} role="tab" aria-selected={sel === i}>
{f.q}
)}
{FAQS[sel].q}
{FAQS[sel].a.split('\n').map((para, k) =>
{para}
)}
);
} else {
body = (
{FAQS.map((f, i) => setOpen(open === i ? -1 : i)} />)}
);
}
return (
);
}
function CTA({ formRef }) {
return (
Ready to embed quality into your workflow?
Book a demo to find out how Qity and Atlassian can accelerate your team.
);
}
/* ---------------- CERTIFICATIONS ---------------- */
function Certs({ tweaks }) {
const light = tweaks && tweaks.certsTheme === 'Light';
return (
Built for regulated industries
{window.STANDARDS.map((s) =>
{s.img ? : }
{s.label}
)}
);
}
/* ---------------- FOOTER ---------------- */
function Footer() {
return (
© 2026 Qity. All rights reserved.
Privacy
);
}
Object.assign(window, { Modules, Integrations, UseCases, Proof, FAQ, Certs, CTA, Footer, DemoForm });