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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions .env.staging.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Template for .env.staging — copy this file to .env.staging next to
# the Dockerfile before running `docker build`, then fill in the real
# staging values. The actual .env.staging is gitignored so secrets
# stay host-side. The image build pipeline (Dockerfile, builder stage)
# requires this file to be present; without it the build falls
# through to .env (development config) and the resulting bundle is
# mislabelled as a development build.

NEXT_PUBLIC_ENV=staging
NEXT_PUBLIC_INDEXING=off

# Public — shipped to the browser bundle.
NEXT_PUBLIC_DOMAIN=https://keepsimple.administration.ae
NEXT_PUBLIC_API_KEY=__fill_in__
NEXT_PUBLIC_STRAPI=https://strapi.keepsimple.io
NEXT_PUBLIC_MIXPANEL_TOKEN=__fill_in__
NEXT_PUBLIC_UXCAT_API=https://staging-uxcat.keepsimple.io/

# Server-only — never exposed to the browser.
STRAPI_URL=https://strapi.keepsimple.io
NEXTAUTH_URL=https://keepsimple.administration.ae
NEXTAUTH_SECRET=__fill_in__

GOOGLE_CLIENT_ID=__fill_in__
GOOGLE_CLIENT_SECRET=__fill_in__

LINKEDIN_CLIENT_ID=__fill_in__
LINKEDIN_CLIENT_SECRET=__fill_in__

DISCORD_CLIENT_ID=__fill_in__
DISCORD_CLIENT_SECRET=__fill_in__
9 changes: 8 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,13 @@ RUN yarn install --frozen-lockfile
FROM base AS builder
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN yarn run build
# Build with APP_ENV=staging so next.config.js loadEnv() reads
# .env.staging during compilation. Without this the build silently
# falls through to .env (NEXT_PUBLIC_ENV=dev, localhost domain) and
# the resulting bundle is mislabelled as a development build.
# The Order must place .env.staging next to the Dockerfile before
# `docker build` (file is gitignored — staging secrets stay host-side).
RUN yarn run build:staging

FROM base AS runner
ENV NODE_ENV=production
Expand All @@ -19,6 +25,7 @@ COPY --from=builder /app/public ./public
COPY --from=builder /app/.next ./.next
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/.env ./.env
COPY --from=builder /app/.env.staging ./.env.staging

EXPOSE 3005
CMD ["yarn", "run", "start:staging"]
6 changes: 5 additions & 1 deletion src/components/Box/Box.module.scss
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
.content {
position: fixed;
bottom: 15px;
/* Lift above the iPhone home-indicator on notched devices.
env() is 0 on devices without a safe area, so desktop layout is
unchanged. Pairs with `viewport-fit=cover` in SeoGenerator. */
bottom: max(15px, env(safe-area-inset-bottom));
right: 15px;
position: -webkit-fixed;
height: auto;
Expand Down Expand Up @@ -61,5 +64,6 @@
margin: 0 15px;
max-width: unset;
width: unset;
bottom: max(15px, env(safe-area-inset-bottom));
}
}
6 changes: 6 additions & 0 deletions src/components/Header/Header.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,12 @@
align-items: center;
justify-content: center;
z-index: 113;
/* Desktop sets gap: 74px which pushes children past the viewport
on phones. Reset for mobile and clip any residual overflow. */
gap: 0;
max-width: 100vw;
max-width: 100dvw;
overflow-x: hidden;

.actions {
display: block;
Expand Down
7 changes: 7 additions & 0 deletions src/components/Modal/Modal.module.scss
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
.overlay {
display: flex;
/* dvh/dvw track the live viewport on iOS Safari (URL-bar collapse
doesn't snap layout). Falls back to vh/vw on browsers without
dvh support. */
width: 100vw;
width: 100dvw;
height: 100vh;
height: 100dvh;
position: fixed;
top: 0;
left: 0;
Expand Down Expand Up @@ -182,7 +187,9 @@
.background {
display: flex;
width: 100vw;
width: 100dvw;
height: 100vh;
height: 100dvh;
position: fixed;
top: 0;
left: 0;
Expand Down
3 changes: 3 additions & 0 deletions src/components/Navbar/Navbar.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -163,9 +163,12 @@
.aside {
position: fixed;
top: 4rem;
/* dvw avoids layout snap when iOS Safari URL bar collapses. */
left: -100vw;
left: -100dvw;
height: calc(100% - 4rem);
width: 100vw;
width: 100dvw;
z-index: 1;
background-color: #ffffff;
background-image: url('/keepsimple_/assets/white-navbar-bg.png');
Expand Down
4 changes: 2 additions & 2 deletions src/components/SeoGenerator/SeoGenerator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ const SeoGenerator: FC<SeoGeneratorProps> = ({
/>
<meta
name="viewport"
content="width=device-width, initial-scale=1, maximum-scale=2"
content="width=device-width, initial-scale=1, maximum-scale=2, viewport-fit=cover"
/>
<title>keep-simple | Error Page</title>
<meta name="description" content={'404 page - page not found'} />
Expand Down Expand Up @@ -183,7 +183,7 @@ const SeoGenerator: FC<SeoGeneratorProps> = ({
<meta charSet="UTF-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1, maximum-scale=2"
content="width=device-width, initial-scale=1, maximum-scale=2, viewport-fit=cover"
/>
<link
rel="preload"
Expand Down
24 changes: 19 additions & 5 deletions src/pages/ai-atlas.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,19 @@ const POL = (r: number, theta: number) => ({
});

/* On touch devices Mouse* events fire synthetically on tap but never
get a leave — without this, hover state would lock on Android. */
get a leave — without this, hover state would lock on Android.
Also: any touch capability disqualifies hover so a tap on iPad / a
touch laptop doesn't accidentally pin-solo (which suppresses the
related-entity highlight ring users expect from desktop hover). */
function useHasHover() {
const [hasHover, setHasHover] = useState(false);
useEffect(() => {
if (typeof window === 'undefined' || !window.matchMedia) return;
const mq = window.matchMedia('(hover: hover) and (pointer: fine)');
const update = () => setHasHover(mq.matches);
const hasTouch =
'ontouchstart' in window ||
(typeof navigator !== 'undefined' && navigator.maxTouchPoints > 0);
const update = () => setHasHover(mq.matches && !hasTouch);
update();
mq.addEventListener?.('change', update);
return () => mq.removeEventListener?.('change', update);
Expand Down Expand Up @@ -645,6 +651,7 @@ function NodeBody({
'node',
node.kind === 'filled' && 'node--filled',
node.redacted && 'node--redacted',
node.diamond && `node--dmd-${node.diamond}`,
active && 'is-active',
highlighted && 'is-glow',
dimmed && 'is-dim',
Expand Down Expand Up @@ -1482,8 +1489,12 @@ function AiAtlasApp() {
};
}, []);

/* hash → view mode (initial load + back/forward) */
useEffect(() => {
/* hash → view mode (initial load + back/forward).
useLayoutEffect runs synchronously after hydration commit and
before paint, so a deep-link to /ai-atlas#security shows the
correct tab on first paint instead of flashing 'environment'
for one frame and then snapping. */
useLayoutEffect(() => {
if (typeof window === 'undefined') return;
const sync = () => {
const hash = window.location.hash.replace(/^#/, '').toLowerCase();
Expand Down Expand Up @@ -1667,7 +1678,10 @@ function AiAtlasApp() {
};
}
const highlightId = linkHoverNode || focusId;
const pinnedSolo = !!focusedNode && !hoverNode && !linkHoverNode;
/* On touch devices there is no hover, so a tap should reveal the same
entity-plus-connections highlight that hovering shows on desktop.
Solo-pin only applies when real hover is available. */
const pinnedSolo = hasHover && !!focusedNode && !hoverNode && !linkHoverNode;

const onSelect = (id: string | null, mode?: string) => {
if (mode === 'hover') {
Expand Down
29 changes: 29 additions & 0 deletions src/styles/ai-atlas.css
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,28 @@ html.scroll-style-atlas::-webkit-scrollbar-thumb:hover {
.dmd.blue { background: var(--blue); }
.dmd.gold { background: var(--gold); }

/* iOS Safari mangles the rotated-div diamond inside foreignObject.
On touch iOS, drop the diamond marker entirely and color the card
outline in the diamond's color instead — same signal, reliable
rendering. Desktop (and non-iOS touch) keep the diamond. */
@media (hover: none) and (pointer: coarse) {
@supports (-webkit-touch-callout: none) {
.canvas--orbital .dmd { display: none; }
.canvas--orbital .node--dmd-red {
outline: 2px solid var(--red);
outline-offset: -1px;
}
.canvas--orbital .node--dmd-blue {
outline: 2px solid var(--blue);
outline-offset: -1px;
}
.canvas--orbital .node--dmd-gold {
outline: 2px solid var(--gold);
outline-offset: -1px;
}
}
}

/* ---------- canvas ---------- */
.canvas--orbital {
position: relative;
Expand Down Expand Up @@ -1061,6 +1083,13 @@ html.scroll-style-atlas::-webkit-scrollbar-thumb:hover {
/* Touch devices (Android, iOS): :hover sticks after tap until the user
taps elsewhere, leaving nodes visually "locked". Neutralize the
hover styles where there is no real hover capability. */
/* Touch devices: never dim node cards. Selecting one card must not
make others vanish — keep the full constellation visible at all
times. Wires and ring labels still dim for focus guidance. */
@media (hover: none) and (pointer: coarse) {
.canvas--orbital .node.is-dim { opacity: 1; }
}

@media (hover: none) {
.node:hover {
background: var(--paper);
Expand Down
3 changes: 3 additions & 0 deletions src/styles/globals.scss
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ body,
html {
padding: 0;
margin: 0;
/* Kill the default grey iOS / Android tap flash. Each interactive
component owns its own pressed state via :active or aria-pressed. */
-webkit-tap-highlight-color: transparent;
}

html {
Expand Down
Loading