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
8 changes: 4 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
"cap:open:android": "npx cap open android"
},
"dependencies": {
"@unisim/sdk": "^0.77.1",
"@unisim/sdk": "^0.79.0",
"konva": "^9.3.20",
"pdf-lib": "^1.17.1",
"pdfjs-dist": "^4.10.38",
Expand Down
26 changes: 12 additions & 14 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -165,12 +165,16 @@ export default function App() {
{doc && <UniversalBar />}
{doc && (
<div className="bg-slate-900 text-white relative z-[45] overflow-x-auto" style={{ paddingRight: 'var(--doc-scrollbar-width, 0px)' }}>
{/* Home + Actions, pinned to the far left of the bar — out in the
margin to the left of the centred tool cluster, so they read as
window chrome rather than editing tools. The universal navbar is
landing-page only; while a doc is open the dark toolbar is the
whole chrome. md+ only: that margin only exists on wider screens.
Mobile keeps a home button inside the cluster below. */}
{/* Home, pinned to the far left of the bar — out in the margin to the
left of the centred tool cluster, so it reads as window chrome
rather than an editing tool. The universal navbar is landing-page
only; while a doc is open the dark toolbar is the whole chrome.
lg+ only: that margin only exists on wider screens. Mobile keeps a
home button inside the cluster below.

Actions used to sit here too. It now rides in the profile pill at
the far right — one control, one dropdown — matching the other
Universal Apps. */}
<div className="hidden lg:flex absolute inset-y-0 left-0 z-10 items-center gap-1 pl-3">
<button
type="button"
Expand All @@ -185,7 +189,6 @@ export default function App() {
<path d="M10 21v-6h4v6" />
</svg>
</button>
<FileMenu variant="toolbar" />
</div>
<div
className="mx-auto w-full min-w-max flex items-center justify-between gap-6 py-2 min-h-[52px]"
Expand All @@ -210,11 +213,6 @@ export default function App() {
<path d="M10 21v-6h4v6" />
</svg>
</button>
{/* Mobile-only Actions. On md+ it lives in the universal navbar,
or — when that bar is hidden — in the far-left chrome group. */}
<div className="lg:hidden">
<FileMenu variant="toolbar" />
</div>
<ToolbarDesktopTools />
</div>
<div className="flex items-center gap-2 justify-end shrink-0 [&>*]:shrink-0">
Expand All @@ -223,7 +221,7 @@ export default function App() {
far right of the bar below, mirroring the Home/Actions chrome
on the far left. */}
<div className="lg:hidden flex items-center gap-2 [&>*]:shrink-0">
<ToolbarUserProfile />
<ToolbarUserProfile actions={<FileMenu variant="rows" />} />
<ChangelogMenu
iconSrc={`${import.meta.env.BASE_URL}unisim-icon.png`}
productFilter="pdf"
Expand All @@ -241,7 +239,7 @@ export default function App() {
className="hidden lg:flex absolute inset-y-0 z-10 items-center gap-2 pr-3"
style={{ right: 'var(--doc-scrollbar-width, 0px)' }}
>
<ToolbarUserProfile />
<ToolbarUserProfile actions={<FileMenu variant="rows" />} />
<ChangelogMenu
iconSrc={`${import.meta.env.BASE_URL}unisim-icon.png`}
productFilter="pdf"
Expand Down
18 changes: 15 additions & 3 deletions src/components/Header/ToolbarUserProfile.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useState, type ComponentProps } from 'react'
import { useState, type ComponentProps, type ReactNode } from 'react'
import {
UserProfile,
SignInDialog,
Expand All @@ -17,8 +17,12 @@ const HUB_LOGIN_HREF = 'https://app.unisim.co.uk/login'
* landing-page only). Mirrors the navbar's exact auth wiring: a plain click on
* "Sign in" opens the in-app <SignInDialog /> so guests sign in via a popup and
* stay in the app; modified clicks still follow hubLoginHref.
*
* Pass `actions` to merge the Actions menu into this control the way the other
* Universal Apps do — one pill, one dropdown, app rows above the account rows —
* instead of an Actions button and an avatar sitting apart in the same bar.
*/
export default function ToolbarUserProfile() {
export default function ToolbarUserProfile({ actions }: { actions?: ReactNode }) {
const { user, loading: userLoading } = useUser()
const { profile, loading: profileLoading } = useProfile()
const { supabase, session } = useUniversal()
Expand Down Expand Up @@ -46,7 +50,15 @@ export default function ToolbarUserProfile() {

return (
<>
<UserProfile {...resolvedUser} menuAlign="right" tier={subscription?.tier} />
<UserProfile
{...resolvedUser}
menuAlign="right"
tier={subscription?.tier}
actions={actions}
// The bar this sits in is slate-900, so the pill takes the dark
// treatment — otherwise it reads as a white chip punched into it.
pillTheme="dark"
/>
<SignInDialog
open={signInOpen}
onClose={() => setSignInOpen(false)}
Expand Down
85 changes: 59 additions & 26 deletions src/components/Toolbar/FileMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,18 @@ import { LANGS, persistLang, readSavedLang, type LangCode } from '../../lib/lang
import { RedactIcon } from '../icons/RedactIcon'

interface Props {
variant?: 'header' | 'toolbar'
/**
* `toolbar` (default) and `header` own their trigger and panel — the dark
* editor bar and the white navbar respectively.
*
* `rows` renders the menu BODY only, no trigger and no panel, for the SDK's
* `actions` slot: since @unisim/sdk 0.78.0 the app's actions and the profile
* are one pill with one dropdown, so this menu supplies its rows and the SDK
* supplies the container. The body is a flat accordion list — every submenu
* expands in place rather than flying out — which is exactly why it can be
* dropped into someone else's panel unchanged.
*/
variant?: 'header' | 'toolbar' | 'rows'
}

export default function FileMenu({ variant = 'toolbar' }: Props) {
Expand Down Expand Up @@ -225,27 +236,17 @@ export default function FileMenu({ variant = 'toolbar' }: Props) {
)
}

return (
<div className="relative" ref={ref}>
<button
onClick={() => setOpen((o) => !o)}
className={triggerClass}
aria-haspopup="true"
aria-expanded={open}
>
Actions
<svg viewBox="0 0 12 12" className={`w-3 h-3 transition-transform ${open ? 'rotate-180' : ''}`} aria-hidden="true">
<path d="M2 4 L6 8 L10 4" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
</svg>
</button>
<input
ref={fileInputRef}
type="file"
accept="application/pdf"
hidden
onChange={onPick}
/>
{open && renderMenu(
const fileInput = (
<input
ref={fileInputRef}
type="file"
accept="application/pdf"
hidden
onChange={onPick}
/>
)

const body = (
<>
{/* Current file name — a non-interactive header at the very top of the
dropdown so the user always knows which PDF the actions apply to. */}
Expand Down Expand Up @@ -601,15 +602,19 @@ export default function FileMenu({ variant = 'toolbar' }: Props) {
</>
)}

{/* Language submenu */}
{/* Language submenu — "Document" is load-bearing, not decoration. This
sets the PDF's own `document.documentElement.lang`; the SDK's
profile menu carries the SUITE-WIDE UI language, and since 0.78.0
both live in this one dropdown. Two rows called "Language" in one
panel is a coin toss for the user. */}
<button
onClick={() => { setLangSubOpen((v) => !v); setShowOtherHint(false) }}
className="w-full flex items-center gap-2 px-3 py-2.5 hover:bg-slate-50 text-sm border-t border-slate-100"
aria-haspopup="true"
aria-expanded={langSubOpen}
>
<span aria-hidden="true">{currentLangOpt.flag}</span>
<span className="flex-1 text-left">Language</span>
<span className="flex-1 text-left">Document language</span>
<span className="text-[11px] text-slate-500 uppercase tracking-wide mr-1">
{currentLangOpt.code === 'other' ? 'EN' : currentLangOpt.code}
</span>
Expand Down Expand Up @@ -651,8 +656,36 @@ export default function FileMenu({ variant = 'toolbar' }: Props) {
)}
</div>
)}
</>,
)}
</>
)

// Rows mode: the SDK's dropdown is the container, so there is no trigger, no
// panel and no `open` state in play (the outside-click and positioning
// effects above are both gated on `open`, which stays false here).
if (variant === 'rows') {
return (
<>
{fileInput}
{body}
</>
)
}

return (
<div className="relative" ref={ref}>
<button
onClick={() => setOpen((o) => !o)}
className={triggerClass}
aria-haspopup="true"
aria-expanded={open}
>
Actions
<svg viewBox="0 0 12 12" className={`w-3 h-3 transition-transform ${open ? 'rotate-180' : ''}`} aria-hidden="true">
<path d="M2 4 L6 8 L10 4" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
</svg>
</button>
{fileInput}
{open && renderMenu(body)}
</div>
)
}