Skip to content

Feat/islands - #554

Draft
kake21 wants to merge 179 commits into
mainfrom
feat/islands
Draft

Feat/islands#554
kake21 wants to merge 179 commits into
mainfrom
feat/islands

Conversation

@kake21

@kake21 kake21 commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Major style overhaul of projectNext.

Extends #526 and #514.
Continues work on #357 and #401.
Implements hamburger menu from #7 issue.

Waiting on SVG support from CMS to color logos in mobile navbar.

  • Standardizes core UI components (Button, TextInput, DateInput, Checkbox, Textarea, NumberInput, SubmitButton) onto a consistent card/gap/rounding system, and adds new Dropdown/SearchableDropdown components used to rebuild the admin user list (sortable, standardized).
  • Various admin page redesigns (sidebar as nested cards, /admin/dots, flairs with animated drag-to-reorder, a new component test page) and smaller fixes (CommitteeCard, HeaderItemPopUp, PopUp, jobads/events zoning, omegaquote bubble tail, edit-mode icon visibility, frontpage preview link).
  • PageTitle context: PageTitleProvider (mounted once in layout.tsx) holds the current title; each page
    renders <PageTitleSetter title="..." />, which pushes the title up via a plain useEffect (set on mount/ch
    ange, clear on unmount) and NavBarTitle reads it back out to render in the nav bar. Adopted across ~15 pages
    (auth, admin, users, events, committees, images, error/not-found, etc.) to avoid prop-drilling a title throug
    h every layout.
  • Changed the colors to a base16 inspired layout.

Images showing the proposed stylechange:

Logged in main page desktop:
image

Profile page desktop:
image

Logged in main page mobile:
Screen Shot 2026-08-05 at 17 46 52

Pofile page mobile:
Screen Shot 2026-08-05 at 17 47 53

kake21 and others added 30 commits April 15, 2026 10:01
kake21 and others added 30 commits August 10, 2026 17:38
…out !important

EventCard's images were overflowing their 420x200 card box and being
badly cropped, because CmsImage's wrapper divs sized themselves off
the image's declared width (a fixed pixel value) rather than their
container.

The declared width is Image.tsx's `width` prop, applied as an inline
style on its wrapper div. Inline styles can only be overridden with
!important, which is why ~10 components across the app were already
carrying `width: 100% !important` just to make an image responsive.
Image now sets that width via a `--image-width` custom property
instead, so it renders through a normal stylesheet rule consumers can
override with ordinary CSS - no !important required. Cleaned up the
now-unnecessary !important from CommitteeCard, ImageCard,
ImagePanelImage, ImageDisplay, CollectionCard, OmbulCover, and
Article's cover image, and applied the same depth-agnostic wrapper-div
fix used for EventCard to Article's cover image, which had the same
latent bug for the no-image-chosen-yet fallback.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Reproduced an intermittent "Maximum update depth exceeded" crash on
article pages, pointing at EditModeNavIcon/EditModeSwitch. Root cause
traced to useEditMode: its registration effect depended on the whole
authResult (and authorizer) object, but useAuthorizer constructs a new
AuthResult on every render (authorizer.auth(...) is unmemoized), and
several callers (e.g. EditableTextField) construct their authorizer
object inline as well. That made the effect's dependency array
different on every render of every component using this hook, so it
re-ran constantly instead of only when authorization actually changed
- amplified further since EditModeContext's Provider value is a fresh
object each render too, cascading the re-renders to every subscriber.

Depend on the primitive authResult.authorized instead of the object,
which only actually changes when authorization status changes. Also
hoisted EditableTextField's inline RequireNothing authorizer to a
module constant, since it takes no dynamic args and was being
reconstructed on every render for no reason.

Reproduced repeatedly before the fix (~1 in 3-4 page loads); toggled
edit mode on/off rapidly several times after the fix with no repeat.
Given the intermittency, treat this as a strong fix rather than a
100%-confirmed one.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
…tructure

- Moved social icons into reusable `socials` section and updated related styling for consistency.
- Removed redundant `.emptyPart` divs and improved spacing with dynamic padding.
- Refactored `InfoBubbles` to simplify icon implementation and align styles.
- Standardized CMS image handling with enhanced container control and fixed overflow issues.
- Added support for children to `Section` components for greater flexibility.
… viewport

height: calc(100dvh - 60px) was a stale approximation of the app shell's
chrome (nav bar, grid gaps, padding) and overflowed the visible content
pane by ~28px. Derive the offset from the same nav-height/gap tokens the
shell layout uses, with a mobile-breakpoint variant, and switch to
min-height so the hero can grow instead of clipping if its content ever
needs more space.
- Replaced static login button with dynamic user-dependent content.
- Added `ProfilePicture` for logged-in users; styled logout state with a distinct button.
- Refined navbar SCSS for conditional styling and responsive layouts.
- Improved frontpage call-to-action buttons with refined styles and animations.
Float the login card on the left as a narrow vertical panel with the
hooded crest logo up top, and use the frontpage hero photo as a large
image panel on the right, instead of the small inverted hat icon.
…ign system

Imports 40 presentational components into the "Ohma Design System" project on
claude.ai/design, so the design agent builds with our real components instead of
generic ones.

projectNext is a Next.js app rather than a component library: there is no library
build, and every component styles itself through *.module.scss, which esbuild
cannot load. .design-sync/stage-pkg.mjs mechanically transforms the real source
into a bundlable package — compiling each SCSS module to *.module.css via sass
(a custom importer resolves the @/styles alias), rewriting only import
specifiers, and shimming next/link and next/navigation. Component bodies are
copied verbatim; nothing here reimplements a component.

All four themes ship with it. stage-pkg.mjs reads theme.ts at build time and
emits a :root[data-theme="<Name>"] block per theme, and the bundle re-exports
themes / applyTheme / subscribeToTheme / getActiveTheme / ThemeName, so designs
can switch palettes exactly as the app's own theme picker does. theme.ts stays
the single source of truth — no palette values are copied.

Contents:
- scope.json    which components sync, and their DS-pane grouping
- stage-pkg.mjs the staging transform
- vendor-inter.mjs vendors Inter, which _fonts.scss names as the site's body
  font but never ships (production currently falls back to system sans)
- previews/     40 authored preview compositions, all graded good
- conventions.md the header prepended to the generated README, telling the
  design agent how to build with Ohma
- NOTES.md      environment quirks, decisions, and re-sync risks

NOTES.md also records real defects surfaced while authoring the previews:
unstyled native Select, FileInput's dark-on-black default, invisible
Slider/TextInput 'secondary' variants, Checkbox losing its styling with
children, BorderButton having no disabled state, SimpleTable's `links` prop
emitting invalid HTML, and a palette mismatch between globals.scss and
themes.Standard. These are reproduced faithfully in the cards rather than
papered over.

Co-Authored-By: Claude Opus 5 <[email protected]>
…ld idiom

FileInput, Select and ColorInput each rendered a bare native control while every
other input used the same floating-label field: surface-raised fill, 2px border
going accent-blue on focus, and a label that floats above the value. They are now
all built the same way — the real input stays focusable but visually hidden, and
the styled surface doubles as its label.

  FileInput   the chosen file name takes the place of the field value (multiple
              files collapse to "N filer valgt"); useId() replaces a per-render
              uuid that risked hydration mismatches
  Select      appearance:none plus the field treatment and a chevron; option
              rows are styled so the native popup is dark rather than white
  ColorInput  the swatch stands in for the value with the hex beside it

All three gained `background="raised"`, and Select/FileInput gained the `white`
colour the other fields already had. No caller signatures changed.

Every field now measures exactly 56px with padding on an 8px grid (border 4 +
24 top + 8 bottom + 20px line box); every $gap multiplier across the eight
stylesheets is a whole number. DateInput needed its height pinned because the
native picker indicator inflated its content box, and the select-backed fields
needed explicit line boxes.

Also fixes a subtle bug shared by Dropdown: globals.scss sets `color` on the
universal selector, which matches a value <span> directly and beats the colour
inherited from its parent — so `color` variants silently did nothing wherever the
value was rendered in a span. Those rules now target the value itself.
SearchableDropdown was unaffected; it renders its value in an <input>.

Adds Color input and Selects sections to /admin/component-test, and updates the
design-system previews to match, including driving real file selection and colour
choices so the variant axes actually vary in the published cards.

.design-sync/stage-pkg.mjs now emits the .d.ts tree itself: it wipes its output
dir (types/ included) and the converter derives the component list from that
tree, so a forgotten tsc silently produced a 12-component bundle instead of 40.

Co-Authored-By: Claude Opus 5 <[email protected]>
The outline button existed twice over: as a BorderButton component used in only
three places, and as a `borderBtn` mixin applied at ten more sites across seven
page stylesheets. In every one of those, it was the secondary action sitting
beside a filled primary — so the tier collapses into the filled secondary style
we already had, and the design system is left with two action tiers instead of
three.

- deletes BorderButton (component + styles) and the `borderBtn` mixin
- adds a `secondaryBtn` mixin holding the filled-secondary treatment, which
  Button.secondary now uses too instead of repeating it
- converts all ten former mixin sites to `secondaryBtn`; CmsLink's explicit
  `.primary` variant becomes the filled primary tier instead
- login: the submit is now `Button color="primary"` (it is the page's main
  action and was previously rendered as an outline, reading as secondary) and
  Feide sign-in is `color="secondary"`
- AddParts: the CMS add-part toolbar becomes `color="secondary"`

Section.module.scss styled `.readMore` identically in its `.blue` and
`:not(.blue)` branches once the outline was gone, so that split is collapsed.

Appearance changes, deliberately: every former outline button is now filled.
That covers the login screen, the frontpage hero and section CTAs, CMS links,
the news archive link and the applications/jobads/flairs secondary actions.

Design system: BorderButton drops out of scope.json and its preview is deleted,
taking the count from 40 to 39 components. conventions.md now tells the design
agent there is no outline/ghost variant and not to invent one.

Co-Authored-By: Claude Opus 5 <[email protected]>
Events were 2-up cards ~460px tall: a 200px cover image, a 150px body and
separate header/footer strips. They are now 96px full-width rows on the 8px
grid — thumbnail, date block, title + tags, then a right-aligned meta column,
with the attendance bar hugging the bottom edge. Roughly five times as many
events fit on a screen.

The cover image is kept as a square thumbnail rather than dropped, so the
CmsImage cover editing (canEdit) still works from the listings.

- EventCardListLayout becomes EventRowListLayout: a stacked single column at
  every breakpoint instead of a 2-column grid
- LoggedInSection takes a `layout` prop; the frontpage events section opts into
  'rows' so the rows are not squeezed into its 3-column card grid
- on mobile the row stays a row and the meta column drops out, rather than
  reflowing back into a stacked card
- month abbreviations are Norwegian; they were full English month names in an
  otherwise Norwegian UI

Also stops the row underlining itself on hover. The row is a single large
anchor, so globals' `a:hover { text-decoration: underline }` underlined every
text run inside it — date, title and all the meta. The background shift is the
hover affordance now.

Verified against the running dev app on /events and the frontpage, at desktop
and 430px. The archive listing shares EventsLandingLayout with /events but has
no seeded data to render.

Co-Authored-By: Claude Opus 5 <[email protected]>
Job ads were ImageCards in a 3-up grid, ~450px tall. They are now 96px
full-width rows matching the event listings: thumbnail, application deadline,
title + company/type, then a right-aligned meta column.

The row geometry moved into a `listRow` mixin that both EventCard and JobAd
include, so the two lists cannot drift apart. Consumers supply .thumb, .lead,
.main, .meta and an optional .bar; EventCard was refactored onto those names.
EventRowListLayout became the generic RowListLayout for the same reason.

JobAd no longer goes through ImageCard. That component is still used by News,
Articles and Education, so it is untouched — only the job-ad rendering changed.

- deadline occupies the lead slot; applicationDeadline is nullable, so ads
  without one read "Løpende" rather than leaving the column blank
- the description is dropped from the listing (there is no room in a row, and
  it is on the detail page); company and job type take its place
- the "Inaktiv" badge moves into the meta column
- frontpage Jobbannonser opts into the row layout, as the events section does
- JobAdInactiveList's .loadingInfo loses `grid-column`, which is inert in a
  flex column

Verified against the running dev app on /career/jobads and the frontpage; the
inactive archive has no seeded data to render.

Co-Authored-By: Claude Opus 5 <[email protected]>
The logged-in landing page painted one surface-base panel behind everything and
stacked its sections inside it. Each section is now its own island — a
surface-base panel with rounded corners — and the page background shows through
the gaps between them.

- LoggedInSection gains a `span` prop: Nyheter and Bilder run full width,
  Hvad der hender and Jobbannonser pair up on a row once there is room
- the hero image and the footer are rounded, and the frontpage wrapper is a
  flex column with a $gap step, so the hero, the content and the footer are
  each separated by one grid unit
- content max-width goes 900px -> 1200px, and the frontpage drops its own
  side padding; what remains at the sides is pagePadding centring, not padding
- the rule under each section title is gone and "Les mer" is now a pill button
  matching the "Ny +" header item (same height, radius, surface and hover)
- social icons are gone from the logged-in page; they remain in the footer

listRow switches from a viewport media query to a container query. Rows now sit
both full width and inside half-width islands, and a viewport query left the
title about 50px in a narrow island on a wide screen — the meta column now
gives way based on the space the row actually has.

Also defines .footer in page.module.scss. LoggedIn and LoggedOut both referenced
styles.footer, which was never declared, so that wrapper rendered with
class="undefined" on both frontpages.

Co-Authored-By: Claude Opus 5 <[email protected]>
Hvad der hender and Jobbannonser share a row on wide screens, but their
flex-basis of 50% kept them side by side all the way down, splitting a phone
screen in two. They now take the full width below the mobile breakpoint.

On phone-width rows the date no longer sits in a column beside the title —
there is not enough horizontal room for it there. The row becomes a grid: the
thumbnail keeps the left edge at full height, and the date runs horizontally
above the title in the space that remains.

Fixes the container-query setup underneath both. `container-type` was on the
row itself, but a container query can only style descendants of its container,
never the container element — so rules targeting the row were silently dead
(the earlier `> .meta { display: none }` worked only because it targets a
child). The container moves to RowListLayout, the list wrapping the rows, which
has the same width and the right semantics. All four row lists get it: the
frontpage islands, the events landing page, the job-ad listing and its archive.

Measured against the compiled CSS in headless chromium: at 1152px and 652px the
row stays flex with a vertical date and its meta column; at 382px it is grid,
the date is horizontal and above the title, both share a left edge clear of the
96px thumbnail, and the row is still 96px tall.

Also includes: "Les mer" becomes "Se flere", and ImageCard's image corners use
$rounding rather than $cardRounding.

Co-Authored-By: Claude Opus 5 <[email protected]>
The two component defects the previous sync documented are fixed: Select is
no longer an unstyled native control and FileInput's default `black` variant
no longer renders dark-on-black. Verified against the freshly built CSS, so
the "prefer Dropdown" steer and the FileInput warning are removed from the
design agent's header and struck through in the notes.

Also record the 56px field shell across all ten fields, correct the stale
`buildCmd` (stage-pkg.mjs emits the .d.ts tree itself), and drop the preview
count to 39 now that BorderButton is retired.

Co-Authored-By: Claude Opus 5 <[email protected]>
ESLint was walking .design-sync/.cache/ and ds-bundle/, neither of which is
tracked, which buried the run under 594 errors from generated code. Ignore
both, and fix the 16 real errors in the tracked previews:

- Previews typed children as React.ReactNode without importing React; use a
  `import type { ReactNode }` instead, as the components in src/ do.
- SimpleTable inlined a styled span per row, pushing three lines past max-len;
  pull Name and Status out so the table body reads as data.
- Dropzone's WithFiles copied the generated files into a second useState via
  an effect, cascading renders. usePngFiles now hands back its own state pair.

Co-Authored-By: Claude Opus 5 <[email protected]>
Extract ArchiveLink as a shared HeaderItems component and use it on
jobads, events and news instead of each page styling its own link.
Move the events page onto PageWrapper/RowListLayout to match jobads
and news, and give LoggedInSection an empty-state placeholder so
paired islands keep their height when a list has nothing to show.

EventCard drops its inline CMS cover-image editing (now a plain
Image) since it no longer needs the canEdit auth check threaded
through every caller.
…on edit mode

Matches the frontpage islands treatment: each interest group and the intro
text paint their own surface-base panel with the page background showing
through the gaps, and the settings popup only appears in edit mode instead
of an always-visible icon. ArticleSection's image sizing needed a fix to
keep working once the group card stopped giving it a fixed-height row to
size against.
…der spacing

Retire the bespoke EventsLandingLayout in favor of PageWrapper, the same
component the events, news, and jobads pages already use, so the archive
page's header, title, and card list match the main events page instead of
diverging from it. Also gives the jobads header buttons the same gapped
wrapper the events/news headers already use, fixing them rendering flush
against each other.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01CLrGtHhr5s8aRSCkxkJijn
Give /users PageWrapper's fillHeight mode so the header and filters stay
pinned while only the table body scrolls. fillHeight previously hard-coded
a transparent background for its one existing consumer (the user settings
layout); decouple that so fillHeight alone keeps the normal solid panel,
and move the transparent look behind the existing transparent prop, applied
explicitly where it's still wanted.

Also tighten every table's corner rounding from $rounding to $gap.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01CLrGtHhr5s8aRSCkxkJijn
The mobile sidebar had open/closed state in JS but no matching CSS, so
it always sat as a fixed 40dvh block over the page with no way to
dismiss it. It's now a proper off-canvas drawer with a toggle button,
backdrop, and click-outside/Escape/navigation to close.

Also fixes a stacking-context bug: .slideBar's unconditional
position: sticky trapped the drawer's fixed-position elements below
the page content regardless of z-index, so the toggle button couldn't
be clicked on sub-pages. Scoped position: sticky to desktop only.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01CLrGtHhr5s8aRSCkxkJijn
Standardize margins for `.registrationButton` and refine layout gap/padding using `$gap` for consistency across event pages.
…n dropdown

Add `overflow-x: hidden` to menu container and update link styles to use consistent width and full alignment.
…der spacing

Introduce a styled back button for the news archive header consistent with the shared layout. Fix mobile spacing for job ad headers and adjust margin behavior for better layout alignment.
…e layouts

Expand `ImageCard` max-width and refine height calculations with `calc()` for consistent scaling. Add mobile-specific margin and padding updates across components and adjust styling for better layout flexibility.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants