Skip to content
Open
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
50 changes: 45 additions & 5 deletions services/web/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,10 @@ object `window.__env_conf`.

## Frameworks Used

- [Mantine](https://mantine.dev/)
- [React Icons](https://react-icons.github.io/react-icons/)
- [shadcn/ui](https://ui.shadcn.com/) — UI components (owned source, copied into `src/components/ui`)
- [Radix UI](https://www.radix-ui.com/) — accessible primitives behind shadcn/ui
- [Tailwind CSS v4](https://tailwindcss.com/) — styling
- [Lucide](https://lucide.dev/) / [React Icons](https://react-icons.github.io/react-icons/)
- [Vite](https://vite.dev/)
- [React](https://facebook.github.io/react/)
- [Bedrock Router](https://github.com/bedrockio/router)
Expand All @@ -92,7 +94,7 @@ object `window.__env_conf`.
## Other Features

- Full login/signup flow with separate dashboard and homepage router
- Uses Mantine as a rich UI element base
- Uses shadcn/ui (Radix UI + Tailwind CSS v4) as the UI component base
- Uses ES6 style React components
- Code hotswapping
- Static server
Expand Down Expand Up @@ -129,6 +131,44 @@ Markdown:
callParams({ method: 'POST', path: '/1/auth/login' });
```

## Updating Styles
## Theming

see [Mantine's Theming](https://mantine.dev/theming/theme-object)
The UI is built on [shadcn/ui](https://ui.shadcn.com/) + Tailwind CSS v4. All
design tokens live in **`src/styles/globals.css`** as CSS variables, defaulting
to the neutral shadcn palette in both light (`:root`) and dark (`.dark`).

### Brand colour

The primary colour is a single knob. To brand the app, change `--primary` and
`--primary-foreground` in **both** `:root` and `.dark` (they are tagged
`BRAND PRIMARY` inline). This drives every primary button, the active tab
indicator and (optionally) the focus ring — nothing else needs to change.

```css
/* src/styles/globals.css — example: a green brand */
:root { --primary: oklch(0.55 0.12 152); --primary-foreground: oklch(0.985 0 0); }
.dark { --primary: oklch(0.62 0.13 152); --primary-foreground: oklch(0.205 0 0); }
```

### Font

The app ships [Geist](https://vercel.com/font) (bundled via
`@fontsource-variable/geist`, so it's stable across platforms). It's wired in
two places — swap both to use a different font:

- the imports in `src/Wrapper.js` (`@fontsource-variable/geist*`)
- the `--font-sans` / `--font-mono` tokens in `src/styles/globals.css`

### Dark mode

`ThemeProvider` (`src/components/ThemeProvider.jsx`) toggles a `.dark` class on
`<html>`; the footer switch flips it. Defaults to light.

### Adding components

shadcn components live in `src/components/ui`. Add more with the CLI (config in
`components.json`):

```bash
npx shadcn@latest add <component>
```
21 changes: 21 additions & 0 deletions services/web/components.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": false,
"tsx": false,
"tailwind": {
"config": "",
"css": "src/styles/globals.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"iconLibrary": "lucide",
"aliases": {
"components": "@/components",
"ui": "@/components/ui",
"lib": "@/lib",
"utils": "@/lib/utils",
"hooks": "@/hooks"
}
}
4 changes: 4 additions & 0 deletions services/web/jsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,11 @@
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true,
"baseUrl": ".",
"paths": {
"@/*": [
"./src/*"
],
"helpers/*": [
"./src/helpers/*"
],
Expand Down
38 changes: 29 additions & 9 deletions services/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,39 +20,59 @@
"@bedrockio/config": "^3.0.0",
"@bedrockio/logger": "^1.1.5",
"@bedrockio/router": "^0.3.2",
"@mantine/core": "^8.3.10",
"@mantine/dates": "^8.3.10",
"@mantine/form": "^8.3.10",
"@mantine/hooks": "^8.3.10",
"@mantine/notifications": "^8.3.10",
"@fontsource-variable/geist": "^5.2.9",
"@fontsource-variable/geist-mono": "^5.2.8",
"@hookform/resolvers": "^5.4.0",
"@mdx-js/rollup": "^3.1.0",
"@radix-ui/react-avatar": "^1.1.11",
"@radix-ui/react-checkbox": "^1.3.3",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-label": "^2.1.8",
"@radix-ui/react-popover": "^1.1.15",
"@radix-ui/react-select": "^2.2.6",
"@radix-ui/react-separator": "^1.1.8",
"@radix-ui/react-slot": "^1.2.4",
"@radix-ui/react-switch": "^1.2.6",
"@radix-ui/react-tabs": "^1.1.13",
"@radix-ui/react-tooltip": "^1.2.8",
"@sentry/browser": "^9.15.0",
"@simplewebauthn/browser": "^13.1.0",
"@tailwindcss/vite": "^4.3.0",
"@uiw/react-json-view": "^2.0.0-alpha.37",
"@vitejs/plugin-react": "^4.4.1",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
"dayjs": "^1.11.13",
"hast-util-sanitize": "^4.0.0",
"hast-util-to-mdast": "^8.4.1",
"input-otp": "^1.4.2",
"less": "^4.3.0",
"lodash-es": "^4.17.21",
"lucide-react": "^1.17.0",
"postcss": "^8.5.3",
"postcss-preset-mantine": "^1.17.0",
"postcss-simple-vars": "^7.0.1",
"prism-react-renderer": "^2.4.1",
"prop-types": "^15.8.1",
"qr.js": "^0.0.0",
"react": "^19.1.1",
"react-day-picker": "^9.5.0",
"react-dom": "^19.1.1",
"react-dropzone": "^14.3.8",
"react-dropzone": "^15.0.0",
"react-helmet-async": "^1.3.0",
"react-hook-form": "^7.77.0",
"react-icons": "^5.5.0",
"react-markdown": "^10.1.0",
"rehype-autolink-headings": "^7.1.0",
"rehype-slug": "^6.0.0",
"remark-gfm": "^4.0.1",
"sonner": "^2.0.7",
"tailwind-merge": "^3.6.0",
"tailwindcss": "^4.3.0",
"tw-animate-css": "^1.4.0",
"vite": "^6.3.5",
"vitest": "^3.1.3"
"vitest": "^3.1.3",
"zod": "^4.4.3"
},
"devDependencies": {
"@bedrockio/eslint-plugin": "^1.5.3",
Expand Down
14 changes: 0 additions & 14 deletions services/web/postcss.config.js

This file was deleted.

18 changes: 9 additions & 9 deletions services/web/src/Wrapper.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,4 @@
// TODO: remove?

// part of integration with mantine
import { BrowserRouter, Route, Routes } from '@bedrockio/router';
import { MantineProvider } from '@mantine/core';
import { Notifications } from '@mantine/notifications';
import dayjs from 'dayjs';
import customParseFormat from 'dayjs/plugin/customParseFormat';
import React, { Suspense } from 'react';
Expand All @@ -12,13 +7,18 @@ import { HelmetProvider } from 'react-helmet-async';
import SessionSwitch from 'helpers/SessionSwitch';
import { SessionProvider, useSession } from 'stores/session';

import { ThemeProvider } from '@/components/ThemeProvider';
import { Toaster } from '@/components/ui/sonner';

import LoadingScreen from 'screens/Loading';
import Unsubscribe from 'screens/Unsubscribe';

import 'utils/sentry';
import { hasAccess } from 'utils/user';

import { theme } from './theme';
import '@fontsource-variable/geist';
import '@fontsource-variable/geist-mono';
import './styles/globals.css';

dayjs.extend(customParseFormat);

Expand All @@ -41,8 +41,8 @@ function AppSwitch() {
export default function Wrapper() {
return (
<SessionProvider>
<MantineProvider theme={theme}>
<Notifications />
<ThemeProvider>
<Toaster />
<BrowserRouter>
<HelmetProvider>
<SessionSwitch>
Expand All @@ -57,7 +57,7 @@ export default function Wrapper() {
</SessionSwitch>
</HelmetProvider>
</BrowserRouter>
</MantineProvider>
</ThemeProvider>
</SessionProvider>
);
}
13 changes: 8 additions & 5 deletions services/web/src/components/Auth/Apple/DisableButton.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { Button } from '@mantine/core';
import { noop } from 'lodash';
import PropTypes from 'prop-types';
import { useState } from 'react';

import { useSession } from 'stores/session';

import { Button } from '@/components/ui/button';
import { Spinner } from '@/components/ui/spinner';

import { disable } from 'utils/auth/apple';

/**
Expand All @@ -15,7 +17,7 @@ import { disable } from 'utils/auth/apple';
* @returns {JSX.Element} The DisableButton component.
*/
export default function DisableButton(props) {
const { onError = noop, onDisabled = noop } = props;
const { onError = noop, onDisabled = noop, ...rest } = props;
const [loading, setLoading] = useState(false);
const { user, updateUser } = useSession();

Expand All @@ -34,11 +36,12 @@ export default function DisableButton(props) {

return (
<Button
color="red"
variant="destructive"
size="sm"
loading={loading}
disabled={loading}
onClick={handleClick}
{...props}>
{...rest}>
{loading && <Spinner />}
Disable
</Button>
);
Expand Down
16 changes: 9 additions & 7 deletions services/web/src/components/Auth/Apple/SignInButton.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { useNavigate } from '@bedrockio/router';
import { ActionIcon, Button } from '@mantine/core';
import { FaApple } from 'react-icons/fa';

import { useSession } from 'stores/session';

import { Button } from '@/components/ui/button';

import { signInWithApple } from 'utils/auth/apple';

export default function AppleSignInButton({
Expand Down Expand Up @@ -34,19 +35,20 @@ export default function AppleSignInButton({

if (type === 'login') {
return (
<ActionIcon
variant="default"
radius="xl"
size={42}
<Button
variant="outline"
size="icon"
className="size-[42px] rounded-full"
title="Use Apple to sign in."
onClick={onClick}>
<FaApple />
</ActionIcon>
</Button>
);
}

return (
<Button onClick={onClick} leftSection={<FaApple />} variant="default">
<Button variant="outline" className="w-full" onClick={onClick}>
<FaApple />
Sign in with Apple
</Button>
);
Expand Down
19 changes: 12 additions & 7 deletions services/web/src/components/Auth/Federated.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import { Divider, Group, Stack } from '@mantine/core';

import { canShowAppleSignin } from 'utils/auth/apple';
import { canShowGoogleSignin } from 'utils/auth/google';
import { canShowPasskey } from 'utils/auth/passkey';
Expand All @@ -21,16 +19,23 @@ export default function Federated(props) {
return null;
}

const Wrapper = type === 'signup' ? Stack : Group;

return (
<>
<Divider labelPosition="center" label="OR" />
<Wrapper grow>
<div className="flex items-center gap-3">
<div className="bg-border h-px flex-1" />
<span className="text-muted-foreground text-xs">OR</span>
<div className="bg-border h-px flex-1" />
</div>
<div
className={
isSignup
? 'flex flex-col gap-3'
: 'flex gap-3 [&>*]:flex-1 [&>*]:justify-center'
}>
{showPasskey && <PasskeyButton {...props} />}
{showGoogle && <GoogleButton {...props} />}
{showApple && <AppleButton {...props} />}
</Wrapper>
</div>
</>
);
}
11 changes: 7 additions & 4 deletions services/web/src/components/Auth/Google/DisableButton.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
import { Button } from '@mantine/core';
import { noop } from 'lodash';
import PropTypes from 'prop-types';
import { useState } from 'react';

import { useSession } from 'stores/session';

import { Button } from '@/components/ui/button';
import { Spinner } from '@/components/ui/spinner';

import { disable } from 'utils/auth/google';

/**
* Button to disable Google authentication for the current user.
* @param {object} props - Component props.
* @param {Function} [props.onError=noop] - Callback function for errors.
* @param {Function} [props.onDisabled=noop] - Callback function after successful disabling.
* @param {object} rest - Remaining props to pass to the Mantine Button.
* @param {object} rest - Remaining props to pass to the Button.
* @returns {JSX.Element}
*/
export default function DisableButton({
Expand Down Expand Up @@ -42,12 +44,13 @@ export default function DisableButton({

return (
<Button
color="red"
variant="destructive"
size="sm"
loading={loading}
disabled={loading}
onClick={handleClick}
{...rest} // Spread remaining props
>
{loading && <Spinner />}
Disable
</Button>
);
Expand Down
Loading
Loading