Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
fb0e88d
feat: 10 new OffSec bias cases + 4 card surfaces (call, document, pos…
manager Jul 16, 2026
ea9bee0
style: OffSec content rewrite, no em-dashes, plain language, fewer em…
manager Jul 16, 2026
74190c5
fix(library): stretch the step connector between both step circles
MaryWylde Aug 17, 2026
ec89891
fix(library): match the tag multi-select trigger to the dropdown
MaryWylde Aug 17, 2026
ed59c12
fix(library): shrink the empty tags box to 44px
MaryWylde Aug 17, 2026
65b79fc
fix(library): size cover hotspots to the buildings they light up
MaryWylde Aug 17, 2026
4c54131
Merge pull request #180 from keepsimpleio/library-polishing
MaryWylde Aug 17, 2026
d3ec4a3
fix(uxcore): gate OffSec everywhere and restore persisted view choice
manager Aug 18, 2026
b961851
fix(uxcore): bias page sync, share previews, rating integrity, Strapi…
manager Aug 18, 2026
da5a3c3
fix(uxcore): search zero-hit state, clear race, result-count labels
manager Aug 18, 2026
080fafb
fix(uxcp): persona save reports truthfully, auth headers built per call
manager Aug 18, 2026
4f34f33
fix(uxcat): answers can no longer be silently lost, resume trusts the…
manager Aug 18, 2026
686d21f
Merge remote-tracking branch 'origin/dev' into feat/fable-bugfix-aug19
manager Aug 19, 2026
c46316c
Merge pull request #181 from keepsimpleio/feat/fable-bugfix-aug19
manager Aug 19, 2026
0850c0f
fix(uxcore): address PR #181 review comments
manager Aug 19, 2026
c39975c
Merge pull request #183 from keepsimpleio/fix/pr181-review-followup
manager Aug 19, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
## Font passport

<!-- font-passport: allowed=8,9,9.14,9.5,10,10.5,11,11.5,11.52,12,12.5,13,13.5,13.6,13.76,14,14.344,14.4,15,16,17,17.6,18,18.4,20,21,22,22.4,23,24,25,26,28,30,32,34,34.56,35,36,38,40,45,48,64,76; floor=8; contrast=4.5 -->

- Allowed sizes: **8, 9, 9.14, 9.5, 10, 10.5, 11, 11.5, 11.52, 12, 12.5, 13, 13.5, 13.6, 13.76, 14, 14.344, 14.4, 15, 16, 17, 17.6, 18, 18.4, 20, 21, 22, 22.4, 23, 24, 25, 26, 28, 30, 32, 34, 34.56, 35, 36, 38, 40, 45, 48, 64, 76px**. Hard floor: **8px**.
- Contrast: **4.5:1** minimum, or **3:1** for text at 18px and above.
- If text does not fit, fix the layout. This passport outranks template and skill defaults.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,6 @@
top: 18px;
left: 46px;
right: 46px;
width: 79.5%;
height: 1px;
background: var(--gray-100);
overflow: hidden;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,25 +8,35 @@
position: relative;
}

// Geometry mirrors the Dropdown trigger so the two fields read as one control
// when they stack in a form: same height, padding, radius, and chevron divider.
.trigger {
width: 100%;
height: 44px;
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
background: var(--white);
border: 1px solid var(--brown-border);
border-radius: 4px;
padding: 12px 16px;
border-radius: 8px;
padding: 0 0 0 12px;
cursor: pointer;
transition: all 0.2s ease;

&:disabled {
opacity: 0.5;
cursor: not-allowed;
}

&.open {
&.open,
&:not(:disabled):hover {
border-color: var(--brown);

.iconWrapper {
border-left: 1px solid var(--brown);
}
}

.placeholder {
Expand All @@ -35,13 +45,27 @@
color: var(--black);
}

.iconWrapper {
border-left: 1px solid var(--brown-border);
height: -webkit-fill-available;
padding: 0 16px;
display: flex;
align-items: center;
justify-content: center;
}

.icon {
flex-shrink: 0;
transform: rotate(90deg);
transition: transform 0.2s ease;

&.rotated {
transform: rotate(-90deg);
}

path {
fill: var(--brown);
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,11 +67,13 @@ export function TagMultiSelect(props: TagMultiSelectProps): JSX.Element {
>
{value.length > 0 ? `${value.length} selected` : placeholder}
</Text>
<ArrowIcon
width={16}
height={16}
className={classNames(styles.icon, { [styles.rotated]: isOpen })}
/>
<div className={styles.iconWrapper}>
<ArrowIcon
width={16}
height={16}
className={classNames(styles.icon, { [styles.rotated]: isOpen })}
/>
</div>
</button>
{isOpen &&
(!portal || menuPos) &&
Expand Down
66 changes: 36 additions & 30 deletions src/components/library/organisms/InteractiveCover/coverHotspots.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,11 +123,39 @@ const makeHotspot = (
const lorem =
'Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa. Cih sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus';

// Hit boxes are sized to the glow silhouette each hotspot lights up, so the
// whole building answers the pointer rather than just its roofline.
//
// Order matters: overlapping triggers share a z-index, so the last one declared
// wins the pointer. The tree (house-2) is the backdrop the buildings sit in
// front of, so it goes first and the buildings keep their own hover areas.
export const coverHotspots: CoverHotspot[] = [
makeHotspot(
'house-2',
{
// The glow here traces the tree, not a building — hence the wide box.
hit: { left: 17.76, top: 0, width: 30.85, height: 55 },
highlight: {
src: '/library/images/hotspots/house-2.svg',
alt: '',
left: 17.98,
top: 0,
width: 47.06,
},
card: { left: 53.0, top: 19.01 },
},
{
libraryName: 'Sarah’s Library',
about: lorem,
bookCount: 88,
videoCount: 34,
songCount: 12,
},
),
makeHotspot(
'house-1',
{
hit: { left: 37.73, top: 11.9, width: 12.55, height: 35.9 },
hit: { left: 41.45, top: 20.02, width: 13.13, height: 29.79 },
highlight: {
src: '/library/images/hotspots/house-1.svg',
alt: '',
Expand All @@ -145,36 +173,15 @@ export const coverHotspots: CoverHotspot[] = [
songCount: 17,
},
{
hit: { left: 45.8, top: 11.9, width: 6 },
// The panorama art sits 0.33% lower here, matching the highlight offset.
hit: { top: 20.35 },
highlight: { left: 45.6, top: 7.9 },
},
),
makeHotspot(
'house-2',
{
hit: { left: 14.13, top: 2, width: 22.41, height: 60 },
highlight: {
src: '/library/images/hotspots/house-2.svg',
alt: '',
left: 17.98,
top: 0,
width: 47.06,
},
card: { left: 53.0, top: 19.01 },
},
{
libraryName: 'Sarah’s Library',
about: lorem,
bookCount: 88,
videoCount: 34,
songCount: 12,
},
{ hit: { left: 34.7824, top: 0 } },
),
makeHotspot(
'house-3',
{
hit: { left: 27.58, top: 43, width: 12.7, height: 34 },
hit: { left: 31.13, top: 50.79, width: 15.57, height: 37.59 },
highlight: {
src: '/library/images/hotspots/house-3.svg',
alt: '',
Expand All @@ -192,14 +199,14 @@ export const coverHotspots: CoverHotspot[] = [
songCount: 9,
},
{
hit: { left: 41, top: 52, width: 7.096, height: 34 },
hit: { top: 49.79 },
highlight: { left: 40.4, top: 17.5 },
},
),
makeHotspot(
'house-4',
{
hit: { left: 17.12, top: 57, width: 10.46, height: 17 },
hit: { left: 20.02, top: 61.73, width: 11.43, height: 22.42 },
highlight: {
src: '/library/images/hotspots/house-4.svg',
alt: '',
Expand All @@ -217,15 +224,15 @@ export const coverHotspots: CoverHotspot[] = [
songCount: 6,
},
{
hit: { left: 35.6, top: 66 },
hit: { top: 60.83 },
highlight: { left: 35.552, top: 28 },
},
),
makeHotspot(
'house-5',
{
// Leader line exits to the LEFT, so the card sits left of the lantern.
hit: { left: 61.94, top: 38, width: 8.96, height: 26 },
hit: { left: 65.44, top: 49.75, width: 8.59, height: 24.69 },
highlight: {
src: '/library/images/hotspots/house-5.svg',
alt: '',
Expand All @@ -242,6 +249,5 @@ export const coverHotspots: CoverHotspot[] = [
videoCount: 14,
songCount: 4,
},
{ hit: { left: 57.7312, top: 44, width: 4.3008, height: 28 } },
),
];
4 changes: 3 additions & 1 deletion src/components/library/organisms/Sidebar/Sidebar.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -133,13 +133,15 @@
display: flex;
flex-wrap: wrap;

// 20px row + the wrapper's 12px padding = a 44px box.
&.tagsEmpty {
height: 50px;
height: 20px;
align-items: center;
}

.emptyTags {
color: var(--gray-medium);
line-height: 20px;
}

.button {
Expand Down
30 changes: 16 additions & 14 deletions src/pages/uxcat/ongoing.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,20 @@
import { useRouter } from 'next/router';
import React, { FC, useEffect, useState } from 'react';

import { TRouter } from '@uxcore/local-types/global';
import { UXCatDataTypes } from '@uxcore/local-types/uxcat-types/types';

import { UXCatConfigs } from '@uxcore/api/uxcat/configs';
import { getFinalTest } from '@uxcore/api/uxcat/final-test';
import { getUXCatStartTest } from '@uxcore/api/uxcat/start-test';
import { getUserInfo } from '@uxcore/api/uxcat/users-me';
import { getUXCatData } from '@uxcore/api/uxcat/uxcat';

import { achievementSlugs } from '@uxcore/data/uxcat/ongoingTest/realTimeAchievements';

import SeoGenerator from '@uxcore/components/SeoGenerator';
import Spinner from '@uxcore/components/Spinner';

import styles from '@uxcore/layouts/OngoingLayout/OngoingLayout.module.scss';
import { achievementSlugs } from '@uxcore/data/uxcat/ongoingTest/realTimeAchievements';
import { TRouter } from '@uxcore/local-types/global';
import { UXCatDataTypes } from '@uxcore/local-types/uxcat-types/types';
import { useRouter } from 'next/router';
import React, { FC, useEffect, useState } from 'react';

import OngoingLayout from 'src/uxcore/layouts/OngoingLayout';

import styles from '@uxcore/layouts/OngoingLayout/OngoingLayout.module.scss';

type OngoingProps = {
configs: {
testExpirationTime: number;
Expand Down Expand Up @@ -84,14 +80,20 @@ const Ongoing: FC<OngoingProps> = ({ configs, uxcatData }) => {
const userInfo = await getUserInfo();
setUserInfo(userInfo);
const ongoingTest = userInfo?.ongoingTest;
if (isFinalTest) {
// When resuming, the server's isFinal flag is the source of truth:
// the localStorage flag can be stale (or absent in another
// browser), and a wrong test length breaks question numbering and
// ends a 30-question final at question 10.
const treatAsFinal = ongoingTest
? !!ongoingTest.isFinal
: isFinalTest;
if (treatAsFinal) {
const testResult = !ongoingTest
? await getFinalTest(accessToken)
: ongoingTest;
setTest(testResult);
setTestLength(30);
}
if (!isFinalTest) {
} else {
const testResult = !ongoingTest
? await getUXCatStartTest(accessToken)
: ongoingTest;
Expand Down
39 changes: 26 additions & 13 deletions src/pages/uxcore/[slug].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
getAdjacentBiasTitles,
mergeBiasesLocalization,
} from '@uxcore/lib/helpers';
import { isOffsecEnabled } from '@uxcore/lib/offsec';
import { getUXCoreTextPaths } from '@uxcore/lib/paths';
import type {
QuestionType,
Expand Down Expand Up @@ -41,8 +42,8 @@ const UXCoreIds: FC<UXCoreProps> = ({
uxcgLocalizedData,
}) => {
const [activeBiasNumber, setActiveBiasNumber] = useState<number>(null);
const [isModalClosed, setIsModalClosed] = useState<boolean>(true);
const [, { isProductView }] = useUXCoreGlobals();
const [, setIsModalClosed] = useState<boolean>(true);
const [, { isProductView, isOffsecView }] = useUXCoreGlobals();
const router = useRouter();
const { locale } = router as TRouter;

Expand All @@ -53,10 +54,19 @@ const UXCoreIds: FC<UXCoreProps> = ({

const strapiQuestions = uxcgLocalizedData?.[locale] ?? [];

// mentionedQuestionsIds comes from Strapi as a JSON string; a null/garbage
// value must degrade to "no mentions", not crash the whole bias page.
const mentionedQuestionIds = useMemo(() => {
try {
const parsed = JSON.parse(currentModalData?.mentionedQuestionsIds);
return Array.isArray(parsed) ? parsed : [];
} catch {
return [];
}
}, [currentModalData?.mentionedQuestionsIds]);

const mentionedQuestions = strapiQuestions.filter(({ attributes }) =>
JSON.parse(currentModalData.mentionedQuestionsIds).includes(
attributes?.number,
),
mentionedQuestionIds.includes(attributes?.number),
);

const openSelectedBias = (number, slug) => {
Expand Down Expand Up @@ -100,7 +110,7 @@ const UXCoreIds: FC<UXCoreProps> = ({
ogImage: {
data: {
attributes: {
url: currentModalData[`OGTags${lang}`]?.OGTags?.ogImage?.data
url: currentModalData[`OGTags${lang}`]?.ogImage?.data
?.attributes?.url,
staticUrl: '/assets/ogImages/UXCore.png',
},
Expand All @@ -111,21 +121,24 @@ const UXCoreIds: FC<UXCoreProps> = ({
}
}, [currentActiveBias.number, locale]);

// Keep the modal's bias in sync with the route. Depending on the prop (not
// mount-only) makes browser Back/Forward land on the right bias instead of
// leaving the modal stuck on the previously viewed one.
useEffect(() => {
setActiveBiasNumber(Number(currentActiveBias.number));
if (!isModalClosed) {
setActiveBiasNumber(null);
}
}, []);
}, [currentActiveBias.number]);

useEffect(() => {
const newHash = isProductView ? '' : 'hr';
const offsecActive = isOffsecEnabled && isOffsecView;
const newHash = offsecActive ? 'offsec' : isProductView ? '' : 'hr';
const currentPath = router.asPath.split('#')[0];
const newUrl = `${currentPath}${newHash ? '#' + newHash : ''}`;
if (router.asPath !== newUrl) {
router.push(newUrl, undefined, { shallow: false });
// Hash-only change: shallow, no scroll — switching the use case inside
// the modal must not refetch the page or jump to the top.
router.push(newUrl, undefined, { shallow: true, scroll: false });
}
}, [isProductView, router.asPath]);
}, [isProductView, isOffsecView, router.asPath]);

const openPage = () => {
router.push(`/uxcore`, undefined, { scroll: true });
Expand Down
Loading
Loading