Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
199 changes: 199 additions & 0 deletions nuxt/components/HiwFit.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
<script setup lang="ts">
// Guided product-fit finder. Four questions map an answer to Edge / Hub / Fleet
// using the same discriminators as the product pages (data origin, unit of scale,
// sponsoring team, protocol needs). Highest score wins; Q1 (data origin) is weighted.
const capture = useCapture()

type Tier = 'edge' | 'hub' | 'fleet'
const TONE: Record<Tier, string> = { edge: '#DA3D0B', hub: '#6366f1', fleet: '#14b8a6' }

interface Q { q: string; weight?: number; options: { label: string; tier: Tier }[] }
const questions: Q[] = [
{ q: 'Where does most of your data come from?', weight: 2, options: [
{ label: 'Machines on the plant floor — PLCs, SCADA, sensors', tier: 'edge' },
{ label: 'Software systems — ERPs, databases, cloud APIs', tier: 'hub' },
{ label: 'Devices deployed across many remote sites', tier: 'fleet' },
] },
{ q: 'What are you trying to scale?', options: [
{ label: 'Production and automation across one or more plants', tier: 'edge' },
{ label: 'Governed integrations that serve the whole business', tier: 'hub' },
{ label: 'A fleet of distributed devices, updated remotely', tier: 'fleet' },
] },
{ q: "Who's driving the project?", options: [
{ label: 'An OT or digitalization team', tier: 'edge' },
{ label: 'Central IT or an integration team', tier: 'hub' },
{ label: 'A hardware OEM or asset operator shipping a product', tier: 'fleet' },
] },
{ q: 'Do you need industrial protocol connectivity — OPC-UA, Modbus, EtherNet/IP?', options: [
{ label: 'Yes — connecting to industrial hardware is core', tier: 'edge' },
{ label: "No — it's software-to-software integration", tier: 'hub' },
{ label: 'It lives on the devices we ship and manage', tier: 'fleet' },
] },
]

const results: Record<Tier, { label: string; eyebrow: string; why: string }> = {
edge: { label: 'FlowFuse Edge', eyebrow: 'For OT & Digitalization Teams', why: "Your data starts at the machines, and you're standardizing production across the plant floor." },
hub: { label: 'FlowFuse Hub', eyebrow: 'For IT & Integration Teams', why: "You're building governed integration between software systems, sponsored by central IT." },
fleet: { label: 'FlowFuse Fleet', eyebrow: 'For Hardware OEMs & Asset Operators', why: 'Distributed devices are your unit of scale — you need remote updates, rollback, and fleet-wide visibility.' },
}

const ORDER: Tier[] = ['edge', 'hub', 'fleet']
const maxScore = questions.reduce((n, q) => n + (q.weight ?? 1), 0)

const step = ref(0)
const answers = ref<(Tier | null)[]>(Array(questions.length).fill(null))

const answeredCount = computed(() => answers.value.filter(Boolean).length)
const finished = computed(() => answers.value.every(Boolean))
const progress = computed(() => Math.round((answeredCount.value / questions.length) * 100))

const scores = computed(() => {
const s: Record<Tier, number> = { edge: 0, hub: 0, fleet: 0 }
answers.value.forEach((a, i) => { if (a) s[a] += questions[i].weight ?? 1 })
return s
})
const best = computed<Tier>(() => {
const s = scores.value
let top = ORDER[0]
for (const t of ORDER) if (s[t] > s[top]) top = t
const tied = ORDER.filter(t => s[t] === s[top])
if (tied.length > 1 && answers.value[0]) return answers.value[0] as Tier
return top
})
const others = computed(() => ORDER.filter(t => t !== best.value))

function choose (tier: Tier) {
answers.value[step.value] = tier
capture('hiw-fit-answer', { step: step.value + 1, choice: tier })
if (step.value < questions.length - 1) setTimeout(() => { step.value++ }, 170)
else capture('hiw-fit-result', { result: best.value })
}
function back () { if (step.value > 0) step.value-- }
function restart () { answers.value = Array(questions.length).fill(null); step.value = 0; capture('hiw-fit-restart', {}) }
</script>

<template>
<div class="hiw-fit">
<div class="hiw-fit__head">
<span class="hiw-fit__eyebrow">Find Your Fit</span>
<h2 class="hiw-fit__title">Which FlowFuse Is Right for You?</h2>
<p class="hiw-fit__sub">Answer four quick questions and we'll point you to the right product to start with — Edge, Hub, or Fleet.</p>
</div>

<div class="hiw-fit__bar" role="progressbar" :aria-valuenow="progress" aria-valuemin="0" aria-valuemax="100">
<span :style="{ width: progress + '%' }"></span>
</div>

<Transition name="fit" mode="out-in">
<!-- QUESTION -->
<div v-if="!finished" :key="step" class="hiw-fit__card">
<div class="hiw-fit__count">Question {{ step + 1 }} of {{ questions.length }}</div>
<div class="hiw-fit__q">{{ questions[step].q }}</div>
<div class="hiw-fit__opts">
<button
v-for="o in questions[step].options"
:key="o.label"
type="button"
class="hiw-fit__opt"
:class="{ 'is-picked': answers[step] === o.tier }"
:style="{ '--t': TONE[o.tier] }"
@click="choose(o.tier)"
>
<span class="hiw-fit__optdot" aria-hidden="true"></span>
<span class="hiw-fit__optlabel">{{ o.label }}</span>
<svg class="hiw-fit__optarrow" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M5 12h14M13 6l6 6-6 6" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
</div>
<button v-if="step > 0" type="button" class="hiw-fit__back" @click="back">&larr; Back</button>
</div>

<!-- RESULT -->
<div v-else key="result" class="hiw-fit__result" :style="{ '--t': TONE[best] }">
<div class="hiw-fit__resulthead">
<span class="hiw-fit__resulteyebrow">Your best fit</span>
<div class="hiw-fit__resultname">{{ results[best].label }}</div>
<div class="hiw-fit__resultrole">{{ results[best].eyebrow }}</div>
<p class="hiw-fit__resultwhy">{{ results[best].why }}</p>
</div>

<div class="hiw-fit__scores">
<div v-for="t in ORDER" :key="t" class="hiw-fit__score" :class="{ 'is-best': t === best }" :style="{ '--t': TONE[t] }">
<span class="hiw-fit__scorelabel">{{ results[t].label.replace('FlowFuse ', '') }}</span>
<span class="hiw-fit__scoretrack"><span :style="{ width: (scores[t] / maxScore * 100) + '%' }"></span></span>
</div>
</div>

<div class="hiw-fit__actions">
<a class="hiw-fit__cta" :href="`/product/${best}/`" @click="capture('hiw-fit-explore', { result: best })">
Explore {{ results[best].label }} <span aria-hidden="true">&rarr;</span>
</a>
<div class="hiw-fit__also">
<span>Also worth a look:</span>
<a v-for="t in others" :key="t" :href="`/product/${t}/`" @click="capture('hiw-fit-other', { tier: t })">{{ results[t].label.replace('FlowFuse ', '') }}</a>
</div>
</div>

<button type="button" class="hiw-fit__restart" @click="restart">&#8635; Start over</button>
</div>
</Transition>
</div>
</template>

<style scoped>
.hiw-fit { position: relative; border-radius: 1.5rem; padding: 2.25rem 2rem; overflow: hidden; background: radial-gradient(120% 120% at 0% 0%, #f5f7ff 0%, #ffffff 55%); border: 1px solid #e6e8f4; box-shadow: 0 18px 44px rgba(2,6,13,0.07); }
.hiw-fit__head { text-align: center; max-width: 34rem; margin: 0 auto 1.5rem; }
.hiw-fit__eyebrow { display: inline-block; font-size: .7rem; font-weight: 800; letter-spacing: .08em; text-transform: uppercase; color: #6366f1; margin-bottom: .5rem; }
.hiw-fit__title { font-size: 1.6rem; font-weight: 700; letter-spacing: -0.02em; color: #0f172a; margin: 0; }
.hiw-fit__sub { font-size: .95rem; color: #64748b; margin: .5rem 0 0; }

.hiw-fit__bar { max-width: 34rem; margin: 0 auto 1.4rem; height: 5px; border-radius: 9999px; background: #eceefb; overflow: hidden; }
.hiw-fit__bar span { display: block; height: 100%; border-radius: 9999px; background: linear-gradient(90deg, #818cf8, #4f46e5); transition: width .4s cubic-bezier(.4,0,.2,1); }

.hiw-fit__card { max-width: 40rem; margin: 0 auto; }
.hiw-fit__count { font-size: .72rem; font-weight: 700; letter-spacing: .05em; text-transform: uppercase; color: #94a3b8; text-align: center; }
.hiw-fit__q { font-size: 1.35rem; font-weight: 700; letter-spacing: -0.01em; color: #0f172a; text-align: center; margin: .5rem 0 1.4rem; line-height: 1.3; }
.hiw-fit__opts { display: flex; flex-direction: column; gap: .7rem; }
.hiw-fit__opt { display: flex; align-items: center; gap: .85rem; width: 100%; text-align: left; padding: 1rem 1.15rem; border-radius: .9rem; border: 1.5px solid #e5e7f0; background: #fff; cursor: pointer; transition: border-color .16s ease, box-shadow .18s ease, transform .12s ease, background .16s ease; }
.hiw-fit__opt:hover { border-color: var(--t); box-shadow: 0 10px 22px rgba(2,6,13,0.08); transform: translateY(-1px); }
.hiw-fit__opt.is-picked { border-color: var(--t); background: color-mix(in srgb, var(--t) 7%, #fff); }
.hiw-fit__optdot { flex: none; width: 1.15rem; height: 1.15rem; border-radius: 9999px; border: 2px solid #d5d9e6; transition: border-color .16s ease, background .16s ease, box-shadow .16s ease; }
.hiw-fit__opt:hover .hiw-fit__optdot, .hiw-fit__opt.is-picked .hiw-fit__optdot { border-color: var(--t); background: var(--t); box-shadow: 0 0 0 4px color-mix(in srgb, var(--t) 18%, transparent); }
.hiw-fit__optlabel { flex: 1; font-size: .95rem; font-weight: 500; color: #1e293b; line-height: 1.4; }
.hiw-fit__optarrow { flex: none; width: 1.1rem; height: 1.1rem; color: var(--t); opacity: 0; transform: translateX(-4px); transition: opacity .16s ease, transform .16s ease; }
.hiw-fit__opt:hover .hiw-fit__optarrow { opacity: 1; transform: translateX(0); }
.hiw-fit__back { display: block; margin: 1.2rem auto 0; font-size: .82rem; font-weight: 600; color: #64748b; background: none; border: none; cursor: pointer; }
.hiw-fit__back:hover { color: #4f46e5; }

/* result */
.hiw-fit__result { max-width: 40rem; margin: 0 auto; text-align: center; }
.hiw-fit__resulthead { position: relative; border-radius: 1.1rem; padding: 1.6rem 1.5rem; background: color-mix(in srgb, var(--t) 8%, #fff); border: 1px solid color-mix(in srgb, var(--t) 28%, transparent); }
.hiw-fit__resulthead::before { content: ""; position: absolute; left: 0; right: 0; top: 0; height: 4px; border-radius: 1.1rem 1.1rem 0 0; background: var(--t); }
.hiw-fit__resulteyebrow { display: inline-block; font-size: .7rem; font-weight: 800; letter-spacing: .08em; text-transform: uppercase; color: var(--t); }
.hiw-fit__resultname { font-size: 1.9rem; font-weight: 800; letter-spacing: -0.02em; color: #0f172a; margin-top: .25rem; }
.hiw-fit__resultrole { font-size: .82rem; font-weight: 600; color: #64748b; margin-top: .2rem; }
.hiw-fit__resultwhy { font-size: .98rem; color: #334155; line-height: 1.55; margin: .8rem auto 0; max-width: 30rem; }

.hiw-fit__scores { display: flex; flex-direction: column; gap: .5rem; margin: 1.4rem auto; max-width: 24rem; }
.hiw-fit__score { display: grid; grid-template-columns: 4.5rem 1fr; align-items: center; gap: .7rem; opacity: .55; }
.hiw-fit__score.is-best { opacity: 1; }
.hiw-fit__scorelabel { font-size: .8rem; font-weight: 700; color: #334155; text-align: right; }
.hiw-fit__scoretrack { height: 8px; border-radius: 9999px; background: #eef0f7; overflow: hidden; }
.hiw-fit__scoretrack span { display: block; height: 100%; border-radius: 9999px; background: var(--t); transition: width .5s cubic-bezier(.4,0,.2,1); }

.hiw-fit__actions { display: flex; flex-direction: column; align-items: center; gap: .9rem; }
.hiw-fit__cta { display: inline-flex; align-items: center; gap: .45rem; padding: .8rem 1.5rem; border-radius: 9999px; background: var(--t); color: #fff; font-weight: 700; font-size: .95rem; text-decoration: none; box-shadow: 0 8px 20px color-mix(in srgb, var(--t) 35%, transparent); transition: transform .12s ease, box-shadow .18s ease; }
.hiw-fit__cta:hover { transform: translateY(-2px); box-shadow: 0 14px 28px color-mix(in srgb, var(--t) 42%, transparent); text-decoration: none; color: #fff; }
.hiw-fit__also { font-size: .85rem; color: #94a3b8; display: inline-flex; flex-wrap: wrap; align-items: center; gap: .55rem; justify-content: center; }
.hiw-fit__also a { font-weight: 700; color: #4338ca; text-decoration: none; }
.hiw-fit__also a:hover { color: #4f46e5; text-decoration: underline; }
.hiw-fit__restart { display: block; margin: 1.3rem auto 0; font-size: .82rem; font-weight: 600; color: #94a3b8; background: none; border: none; cursor: pointer; }
.hiw-fit__restart:hover { color: #4f46e5; }

/* transitions */
.fit-enter-active, .fit-leave-active { transition: opacity .26s ease, transform .26s cubic-bezier(.4,0,.2,1); }
.fit-enter-from { opacity: 0; transform: translateY(10px); }
.fit-leave-to { opacity: 0; transform: translateY(-8px); }
@media (prefers-reduced-motion: reduce) {
.fit-enter-active, .fit-leave-active, .hiw-fit__bar span, .hiw-fit__scoretrack span { transition: none; }
}
</style>
49 changes: 49 additions & 0 deletions nuxt/components/HiwFlow.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<script setup lang="ts">
// Animated flow connector between diagram lanes.
// A faint track + continuously flowing dashes (stroke-dashoffset) + a GPU-driven
// glow "packet" (transform: translateY) travelling the line. Reduced-motion aware.
const props = defineProps<{ label: string; tone?: 'ot' | 'it' | 'fleet' | 'broker' | 'indigo' }>()
const TONE: Record<string, string> = {
ot: '#DA3D0B', it: '#6366f1', fleet: '#14b8a6', broker: '#0d9488', indigo: '#6366f1',
}
const color = computed(() => TONE[props.tone ?? 'indigo'])
</script>

<template>
<div class="hiw-flow" :style="{ '--flow': color }">
<svg class="hiw-flow__svg" width="24" height="104" viewBox="0 0 24 104" preserveAspectRatio="none" aria-hidden="true">
<line x1="12" y1="2" x2="12" y2="102" class="hiw-flow__track" />
<line x1="12" y1="2" x2="12" y2="102" class="hiw-flow__dash" />
</svg>
<span class="hiw-flow__dot" aria-hidden="true"></span>
<span class="hiw-flow__pill">{{ label }}</span>
<svg class="hiw-flow__arrow" width="16" height="9" viewBox="0 0 16 9" aria-hidden="true">
<path d="M1.5 1.5L8 7.5l6.5-6" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
</svg>
</div>
</template>

<style scoped>
.hiw-flow { position: relative; height: 104px; display: grid; place-items: center; margin: .2rem 0; }
.hiw-flow__svg { position: absolute; top: 0; left: 50%; transform: translateX(-50%); width: 24px; height: 104px; overflow: visible; }
.hiw-flow__track { stroke: #dfe3ee; stroke-width: 2; stroke-linecap: round; }
.hiw-flow__dash { stroke: var(--flow); stroke-width: 2.5; stroke-linecap: round; stroke-dasharray: 2 11; stroke-dashoffset: 0; opacity: .85; animation: hiw-flow-dash .85s linear infinite; }

/* glow packet — transform-animated for buttery smoothness */
.hiw-flow__dot { position: absolute; top: 0; left: 50%; width: 9px; height: 9px; border-radius: 9999px; background: var(--flow); box-shadow: 0 0 10px 2px color-mix(in srgb, var(--flow) 55%, transparent); transform: translate(-50%, 0); animation: hiw-flow-dot 2.1s linear infinite; will-change: transform, opacity; }

.hiw-flow__pill { position: relative; z-index: 2; padding: .42rem .9rem; border-radius: 9999px; background: #fff; border: 1px solid #e2e6f0; box-shadow: 0 2px 8px rgba(2,6,13,0.07); font-size: .63rem; font-weight: 800; letter-spacing: .06em; text-transform: uppercase; color: #64748b; text-align: center; max-width: 92%; }
.hiw-flow__arrow { position: absolute; bottom: 4px; left: 50%; transform: translateX(-50%); z-index: 1; color: var(--flow); opacity: .75; }

@keyframes hiw-flow-dash { to { stroke-dashoffset: -13; } }
@keyframes hiw-flow-dot {
0% { transform: translate(-50%, 2px); opacity: 0; }
14% { opacity: 1; }
86% { opacity: 1; }
100% { transform: translate(-50%, 96px); opacity: 0; }
}
@media (prefers-reduced-motion: reduce) {
.hiw-flow__dash { animation: none; }
.hiw-flow__dot { display: none; }
}
</style>
41 changes: 41 additions & 0 deletions nuxt/components/HiwGoDeeper.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<script setup lang="ts">
// End-of-section hand-off to the deep technical reference (Drew's Application Guide).
// A tour stop consolidates several technical topics, so this takes a list of links.
const props = defineProps<{ links: { slug: string; title: string }[] }>()
const capture = useCapture()
</script>

<template>
<div class="hiw-godeeper">
<div class="hiw-godeeper__head">
<span class="hiw-godeeper__ic" aria-hidden="true">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7"><path d="M4 5.5A1.5 1.5 0 015.5 4H11v16H5.5A1.5 1.5 0 014 18.5v-13zM20 5.5A1.5 1.5 0 0018.5 4H13v16h5.5a1.5 1.5 0 001.5-1.5v-13z" stroke-linejoin="round"/></svg>
</span>
<div>
<div class="hiw-godeeper__eyebrow">Go deeper · technical reference</div>
<div class="hiw-godeeper__sub">The technical documentation covers the how-to in detail.</div>
</div>
</div>
<div class="hiw-godeeper__links">
<a
v-for="l in links"
:key="l.slug"
:href="`/application-guide/flowfuse/${l.slug}/`"
class="hiw-godeeper__link"
@click="capture('hiw-docs', { section: l.slug })"
>{{ l.title }} <span aria-hidden="true">&rarr;</span></a>
</div>
</div>
</template>

<style scoped>
.hiw-godeeper { margin-top: 1.75rem; padding: 1.15rem 1.3rem; border-radius: .9rem; background: linear-gradient(105deg, #eef2ff, #f7f8ff 75%); border: 1px solid #e0e4ff; }
.hiw-godeeper__head { display: flex; align-items: center; gap: .8rem; }
.hiw-godeeper__ic { flex: none; display: grid; place-items: center; width: 2.2rem; height: 2.2rem; border-radius: .6rem; background: #fff; color: #4f46e5; border: 1px solid #e0e4ff; }
.hiw-godeeper__ic svg { width: 1.2rem; height: 1.2rem; }
.hiw-godeeper__eyebrow { font-size: .66rem; font-weight: 800; letter-spacing: .06em; text-transform: uppercase; color: #6366f1; }
.hiw-godeeper__sub { font-size: .85rem; color: #64748b; margin-top: .1rem; }
.hiw-godeeper__links { display: flex; flex-wrap: wrap; gap: .5rem; margin-top: .9rem; }
.hiw-godeeper__link { display: inline-flex; align-items: center; gap: .3rem; font-size: .82rem; font-weight: 600; color: #4338ca; background: #fff; border: 1px solid #dfe3f3; border-radius: 9999px; padding: .4rem .8rem; text-decoration: none; transition: border-color .15s ease, background .15s ease; }
.hiw-godeeper__link:hover { border-color: #a5b4fc; background: #f5f7ff; text-decoration: none; }
</style>
Loading
Loading