Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| acb024b64a | |||
| 31dcbddcf3 | |||
| d6ca3ed202 | |||
| 249011156e | |||
| dbbb4b356e | |||
| 4714067a51 | |||
| 6cc13fa39a | |||
| 1c93f04b1f | |||
| 71811155b3 | |||
| 5d35161ae3 | |||
| 0b6f58bf9c | |||
| 79519fef26 | |||
| d7b1fbaf28 | |||
| 26bc67d1b4 | |||
| b783a8bac6 |
@@ -160,6 +160,21 @@ jobs:
|
||||
- name: Run tests
|
||||
run: ~/.local/bin/pytest -n 2
|
||||
|
||||
lint-front:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install dependencies
|
||||
run: cd src/frontend/ && npm ci
|
||||
|
||||
- name: Check linting
|
||||
run: cd src/frontend/ && npm run lint
|
||||
|
||||
- name: Check format
|
||||
run: cd src/frontend/ && npm run check
|
||||
|
||||
i18n-crowdin:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
|
||||
@@ -48,7 +48,7 @@ WAIT_DB = @$(COMPOSE_RUN) dockerize -wait tcp://$(DB_HOST):$(DB_PORT
|
||||
|
||||
# -- Backend
|
||||
MANAGE = $(COMPOSE_RUN_APP) python manage.py
|
||||
MAIL_YARN = $(COMPOSE_RUN) -w /app/src/mail node yarn # FIXME : use npm
|
||||
MAIL_NPM = $(COMPOSE_RUN) -w /app/src/mail node npm
|
||||
|
||||
# -- Frontend
|
||||
PATH_FRONT = ./src/frontend
|
||||
@@ -259,19 +259,19 @@ i18n-generate-and-upload: \
|
||||
# -- Mail generator
|
||||
|
||||
mails-build: ## Convert mjml files to html and text
|
||||
@$(MAIL_YARN) build
|
||||
@$(MAIL_NPM) run build
|
||||
.PHONY: mails-build
|
||||
|
||||
mails-build-html-to-plain-text: ## Convert html files to text
|
||||
@$(MAIL_YARN) build-html-to-plain-text
|
||||
@$(MAIL_NPM) run build-html-to-plain-text
|
||||
.PHONY: mails-build-html-to-plain-text
|
||||
|
||||
mails-build-mjml-to-html: ## Convert mjml files to html and text
|
||||
@$(MAIL_YARN) build-mjml-to-html
|
||||
@$(MAIL_NPM) run build-mjml-to-html
|
||||
.PHONY: mails-build-mjml-to-html
|
||||
|
||||
mails-install: ## install the mail generator
|
||||
@$(MAIL_YARN) install
|
||||
@$(MAIL_NPM) install
|
||||
.PHONY: mails-install
|
||||
|
||||
|
||||
|
||||
+19
-11
@@ -3,20 +3,28 @@
|
||||
Whenever we are cooking a new release (e.g. `4.18.1`) we should follow a standard procedure described below:
|
||||
|
||||
1. Create a new branch named: `release/4.18.1`.
|
||||
2. Bump the release number for backend project, frontend projects, and Helm files:
|
||||
2. Bump the release number for backend project, frontend projects, and Helm files:
|
||||
|
||||
- for backend, update the version number by hand in `pyproject.toml`,
|
||||
- for each frontend projects (`src/frontend`, `src/mail`), run `npm version 4.18.1` in their directory. This will update both their `package.json` and `package-lock.json` for you,
|
||||
- for Helm, update Docker image tag in files located at `src/helm/env.d` for both `preprod` and `production` environments:
|
||||
- for backend, update the version number by hand in `pyproject.toml`,
|
||||
- for each frontend projects (`src/frontend`, `src/mail`), run `npm version 4.18.1` in their directory. This will update both their `package.json` and `package-lock.json` for you,
|
||||
- for Helm, update Docker image tag in files located at `src/helm/env.d` for both `preprod` and `production` environments:
|
||||
|
||||
```yaml
|
||||
image:
|
||||
repository: lasuite/meet-backend
|
||||
pullPolicy: Always
|
||||
tag: "v4.18.1" # Replace with your new version number, without forgetting the "v" prefix
|
||||
```
|
||||
```yaml
|
||||
image:
|
||||
repository: lasuite/meet-backend
|
||||
pullPolicy: Always
|
||||
tag: "v4.18.1" # Replace with your new version number, without forgetting the "v" prefix
|
||||
|
||||
...
|
||||
|
||||
frontend:
|
||||
image:
|
||||
repository: lasuite/meet-frontend
|
||||
pullPolicy: Always
|
||||
tag: "v4.18.1" # Replace with your new version number, without forgetting the "v" prefix
|
||||
```
|
||||
|
||||
The new images don't exist _yet_: they will be created automatically later in the process.
|
||||
The new images don't exist _yet_: they will be created automatically later in the process.
|
||||
|
||||
3. ~~Update the project's `Changelog` following the [keepachangelog](https://keepachangelog.com/en/0.3.0/) recommendations~~ _we don't keep a changelog yet for now as the project is still in its infancy. Soon™!_
|
||||
4. Commit your changes with the following format: the 🔖 release emoji, the type of release (patch/minor/patch) and the release version:
|
||||
|
||||
@@ -25,7 +25,7 @@ license = { file = "LICENSE" }
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
dependencies = [
|
||||
"boto3==1.34.153",
|
||||
"boto3==1.34.154",
|
||||
"Brotli==1.1.0",
|
||||
"celery[redis]==5.4.0",
|
||||
"django-configurations==2.5.1",
|
||||
|
||||
@@ -7,6 +7,7 @@ module.exports = {
|
||||
'plugin:react-hooks/recommended',
|
||||
'plugin:@tanstack/eslint-plugin-query/recommended',
|
||||
'plugin:jsx-a11y/recommended',
|
||||
'prettier'
|
||||
],
|
||||
ignorePatterns: ['dist', '.eslintrc.cjs', 'styled-system'],
|
||||
parser: '@typescript-eslint/parser',
|
||||
|
||||
Generated
+33
-3
@@ -37,10 +37,12 @@
|
||||
"@typescript-eslint/parser": "7.13.1",
|
||||
"@vitejs/plugin-react": "4.3.1",
|
||||
"eslint": "8.57.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-jsx-a11y": "6.9.0",
|
||||
"eslint-plugin-react-hooks": "4.6.2",
|
||||
"eslint-plugin-react-refresh": "0.4.7",
|
||||
"postcss": "8.4.39",
|
||||
"prettier": "3.3.3",
|
||||
"typescript": "5.5.2",
|
||||
"vite": "5.3.1",
|
||||
"vite-tsconfig-paths": "4.3.2"
|
||||
@@ -452,6 +454,7 @@
|
||||
},
|
||||
"node_modules/@clack/prompts/node_modules/is-unicode-supported": {
|
||||
"version": "1.3.0",
|
||||
"dev": true,
|
||||
"inBundle": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -1588,6 +1591,21 @@
|
||||
"node": "^10 || ^12 || >=14"
|
||||
}
|
||||
},
|
||||
"node_modules/@pandacss/node/node_modules/prettier": {
|
||||
"version": "3.2.5",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.5.tgz",
|
||||
"integrity": "sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"prettier": "bin/prettier.cjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/prettier/prettier?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/@pandacss/parser": {
|
||||
"version": "0.41.0",
|
||||
"resolved": "https://registry.npmjs.org/@pandacss/parser/-/parser-0.41.0.tgz",
|
||||
@@ -5700,6 +5718,18 @@
|
||||
"url": "https://opencollective.com/eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-config-prettier": {
|
||||
"version": "9.1.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz",
|
||||
"integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"eslint-config-prettier": "bin/cli.js"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"eslint": ">=7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-plugin-jsx-a11y": {
|
||||
"version": "6.9.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.9.0.tgz",
|
||||
@@ -8919,9 +8949,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/prettier": {
|
||||
"version": "3.2.5",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.5.tgz",
|
||||
"integrity": "sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==",
|
||||
"version": "3.3.3",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz",
|
||||
"integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"prettier": "bin/prettier.cjs"
|
||||
|
||||
@@ -8,7 +8,9 @@
|
||||
"build": "panda codegen && tsc -b && vite build",
|
||||
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
||||
"preview": "vite preview",
|
||||
"i18n:extract": "npx i18next -c i18next-parser.config.json"
|
||||
"i18n:extract": "npx i18next -c i18next-parser.config.json",
|
||||
"format": "prettier --write ./src",
|
||||
"check": "prettier --check ./src"
|
||||
},
|
||||
"dependencies": {
|
||||
"@livekit/components-react": "2.3.3",
|
||||
@@ -40,10 +42,12 @@
|
||||
"@typescript-eslint/parser": "7.13.1",
|
||||
"@vitejs/plugin-react": "4.3.1",
|
||||
"eslint": "8.57.0",
|
||||
"eslint-config-prettier": "9.1.0",
|
||||
"eslint-plugin-jsx-a11y": "6.9.0",
|
||||
"eslint-plugin-react-hooks": "4.6.2",
|
||||
"eslint-plugin-react-refresh": "0.4.7",
|
||||
"postcss": "8.4.39",
|
||||
"prettier": "3.3.3",
|
||||
"typescript": "5.5.2",
|
||||
"vite": "5.3.1",
|
||||
"vite-tsconfig-paths": "4.3.2"
|
||||
|
||||
@@ -199,6 +199,7 @@ const config: Config = {
|
||||
text: { value: '{colors.white}' },
|
||||
subtle: { value: '{colors.red.100}' },
|
||||
'subtle-text': { value: '{colors.red.700}' },
|
||||
...pandaPreset.theme.tokens.colors.red,
|
||||
},
|
||||
success: {
|
||||
DEFAULT: { value: '{colors.green.700}' },
|
||||
|
||||
+14
-12
@@ -6,13 +6,13 @@ import { QueryClientProvider } from '@tanstack/react-query'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useLang } from 'hoofd'
|
||||
import { Switch, Route } from 'wouter'
|
||||
import { I18nProvider } from 'react-aria-components'
|
||||
import { Layout } from './layout/Layout'
|
||||
import { NotFoundScreen } from './components/NotFoundScreen'
|
||||
import { routes } from './routes'
|
||||
import './i18n/init'
|
||||
import { silenceLiveKitLogs } from "@/utils/livekit.ts";
|
||||
import { queryClient } from "@/api/queryClient";
|
||||
|
||||
import { silenceLiveKitLogs } from '@/utils/livekit.ts'
|
||||
import { queryClient } from '@/api/queryClient'
|
||||
|
||||
function App() {
|
||||
const { i18n } = useTranslation()
|
||||
@@ -24,15 +24,17 @@ function App() {
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<Suspense fallback={null}>
|
||||
<Layout>
|
||||
<Switch>
|
||||
{Object.entries(routes).map(([, route], i) => (
|
||||
<Route key={i} path={route.path} component={route.Component} />
|
||||
))}
|
||||
<Route component={NotFoundScreen} />
|
||||
</Switch>
|
||||
</Layout>
|
||||
<ReactQueryDevtools initialIsOpen={false} />
|
||||
<I18nProvider locale={i18n.language}>
|
||||
<Layout>
|
||||
<Switch>
|
||||
{Object.entries(routes).map(([, route], i) => (
|
||||
<Route key={i} path={route.path} component={route.Component} />
|
||||
))}
|
||||
<Route component={NotFoundScreen} />
|
||||
</Switch>
|
||||
</Layout>
|
||||
<ReactQueryDevtools initialIsOpen={false} />
|
||||
</I18nProvider>
|
||||
</Suspense>
|
||||
</QueryClientProvider>
|
||||
)
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
import { QueryClient } from "@tanstack/react-query";
|
||||
import { QueryClient } from '@tanstack/react-query'
|
||||
|
||||
export const queryClient = new QueryClient()
|
||||
|
||||
@@ -4,7 +4,10 @@ import { useTranslation } from 'react-i18next'
|
||||
import { Center } from '@/styled-system/jsx'
|
||||
import { Text } from '@/primitives'
|
||||
|
||||
export const ErrorScreen = ({ title, body }: {
|
||||
export const ErrorScreen = ({
|
||||
title,
|
||||
body,
|
||||
}: {
|
||||
title?: string
|
||||
body?: string
|
||||
}) => {
|
||||
|
||||
@@ -4,5 +4,7 @@ export const authUrl = ({
|
||||
silent = false,
|
||||
returnTo = window.location.href,
|
||||
} = {}) => {
|
||||
return apiUrl(`/authenticate?silent=${encodeURIComponent(silent)}&returnTo=${encodeURIComponent(returnTo)}`)
|
||||
return apiUrl(
|
||||
`/authenticate?silent=${encodeURIComponent(silent)}&returnTo=${encodeURIComponent(returnTo)}`
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
import { authUrl } from "@/features/auth";
|
||||
import { authUrl } from '@/features/auth'
|
||||
|
||||
const SILENT_LOGIN_RETRY_KEY = 'silent-login-retry'
|
||||
|
||||
const isRetryAllowed = () => {
|
||||
const lastRetryDate = localStorage.getItem(SILENT_LOGIN_RETRY_KEY);
|
||||
const lastRetryDate = localStorage.getItem(SILENT_LOGIN_RETRY_KEY)
|
||||
if (!lastRetryDate) {
|
||||
return true;
|
||||
return true
|
||||
}
|
||||
const now = new Date();
|
||||
const now = new Date()
|
||||
return now.getTime() > Number(lastRetryDate)
|
||||
}
|
||||
|
||||
const setNextRetryTime = (retryIntervalInSeconds: number) => {
|
||||
const now = new Date()
|
||||
const nextRetryTime = now.getTime() + (retryIntervalInSeconds * 1000);
|
||||
localStorage.setItem(SILENT_LOGIN_RETRY_KEY, String(nextRetryTime));
|
||||
const nextRetryTime = now.getTime() + retryIntervalInSeconds * 1000
|
||||
localStorage.setItem(SILENT_LOGIN_RETRY_KEY, String(nextRetryTime))
|
||||
}
|
||||
|
||||
const initiateSilentLogin = () => {
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
export { useAudioOutputs } from './utils/useAudioOutputs'
|
||||
export { usePersistedMediaDeviceSelect } from './utils/usePersistedMediaDeviceSelect'
|
||||
@@ -0,0 +1,36 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
|
||||
const getOutputDevices = () => {
|
||||
return navigator.mediaDevices
|
||||
.getUserMedia({ audio: true, video: false })
|
||||
.then(() => navigator.mediaDevices.enumerateDevices())
|
||||
.then((devices) => devices.filter(({ kind }) => kind === 'audiooutput'))
|
||||
}
|
||||
|
||||
/**
|
||||
* custom hook to fetch audio outputs
|
||||
*
|
||||
* this is used instead of livekit's useMediaDevices because the livekit integrated one seems to request
|
||||
* outputs in a weird order, resulting in empty results in firefox if we didn't ask for input before
|
||||
*/
|
||||
export const useAudioOutputs = () => {
|
||||
const [audioOutputs, setAudioOutputs] = useState<MediaDeviceInfo[]>([])
|
||||
useEffect(() => {
|
||||
const retrieveOutputDevices = () => {
|
||||
getOutputDevices()
|
||||
.then(setAudioOutputs)
|
||||
.catch((error) => {
|
||||
console.error('Audio outputs retrieval error :', error)
|
||||
})
|
||||
}
|
||||
retrieveOutputDevices()
|
||||
const onDeviceChange = () => {
|
||||
retrieveOutputDevices()
|
||||
}
|
||||
navigator?.mediaDevices?.addEventListener('devicechange', onDeviceChange)
|
||||
return () => {
|
||||
navigator.mediaDevices.removeEventListener('devicechange', onDeviceChange)
|
||||
}
|
||||
}, [])
|
||||
return audioOutputs
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { useMediaDeviceSelect } from '@livekit/components-react'
|
||||
import { settingsStore } from '@/features/settings'
|
||||
|
||||
/**
|
||||
* wrap livekit's useMediaDeviceSelect to automatically save in our devices state user selection
|
||||
*
|
||||
* note: audiooutput devices are not handled here as we dont use useMediaDeviceSelect for them
|
||||
*/
|
||||
export const usePersistedMediaDeviceSelect = (
|
||||
...args: Parameters<typeof useMediaDeviceSelect>
|
||||
): ReturnType<typeof useMediaDeviceSelect> => {
|
||||
const results = useMediaDeviceSelect(...args)
|
||||
const originalSetter = results.setActiveMediaDevice
|
||||
results.setActiveMediaDevice = (
|
||||
...activeMediaDeviceArgs: Parameters<typeof results.setActiveMediaDevice>
|
||||
) => {
|
||||
const id = activeMediaDeviceArgs[0]
|
||||
if (args[0].kind === 'audioinput') {
|
||||
settingsStore.devices.micDeviceId = id
|
||||
}
|
||||
if (args[0].kind === 'videoinput') {
|
||||
settingsStore.devices.cameraDeviceId = id
|
||||
}
|
||||
return originalSetter(...activeMediaDeviceArgs)
|
||||
}
|
||||
return results
|
||||
}
|
||||
@@ -19,8 +19,8 @@ export const Home = () => {
|
||||
navigateTo('room', data.slug, {
|
||||
state: { create: true, initialRoomData: data },
|
||||
})
|
||||
}
|
||||
});
|
||||
},
|
||||
})
|
||||
|
||||
return (
|
||||
<UserAware>
|
||||
@@ -43,9 +43,9 @@ export const Home = () => {
|
||||
onPress={
|
||||
isLoggedIn
|
||||
? async () => {
|
||||
const slug = generateRoomId()
|
||||
await createRoom({slug})
|
||||
}
|
||||
const slug = generateRoomId()
|
||||
await createRoom({ slug })
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
href={isLoggedIn ? undefined : authUrl()}
|
||||
|
||||
@@ -9,6 +9,6 @@ export type ApiRoom = {
|
||||
token: string
|
||||
}
|
||||
configuration?: {
|
||||
[key: string]: string | number | boolean;
|
||||
[key: string]: string | number | boolean
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,25 +1,26 @@
|
||||
import { useMutation, UseMutationOptions } from "@tanstack/react-query";
|
||||
import { fetchApi } from '@/api/fetchApi';
|
||||
import { ApiError } from "@/api/ApiError";
|
||||
import { ApiRoom } from "./ApiRoom";
|
||||
import { useMutation, UseMutationOptions } from '@tanstack/react-query'
|
||||
import { fetchApi } from '@/api/fetchApi'
|
||||
import { ApiError } from '@/api/ApiError'
|
||||
import { ApiRoom } from './ApiRoom'
|
||||
|
||||
export interface CreateRoomParams {
|
||||
slug: string;
|
||||
slug: string
|
||||
}
|
||||
|
||||
const createRoom = ({slug}: CreateRoomParams): Promise<ApiRoom> => {
|
||||
const createRoom = ({ slug }: CreateRoomParams): Promise<ApiRoom> => {
|
||||
return fetchApi(`rooms/`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
name: slug,
|
||||
}),
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
export function useCreateRoom(options?: UseMutationOptions<ApiRoom, ApiError, CreateRoomParams>) {
|
||||
export function useCreateRoom(
|
||||
options?: UseMutationOptions<ApiRoom, ApiError, CreateRoomParams>
|
||||
) {
|
||||
return useMutation<ApiRoom, ApiError, CreateRoomParams>({
|
||||
mutationFn: createRoom,
|
||||
onSuccess: options?.onSuccess,
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import {
|
||||
formatChatMessageLinks,
|
||||
LiveKitRoom,
|
||||
VideoConference,
|
||||
type LocalUserChoices,
|
||||
} from '@livekit/components-react'
|
||||
import { Room, RoomOptions } from 'livekit-client'
|
||||
import { keys } from '@/api/queryKeys'
|
||||
@@ -18,6 +17,7 @@ import { fetchRoom } from '../api/fetchRoom'
|
||||
import { ApiRoom } from '../api/ApiRoom'
|
||||
import { useCreateRoom } from '../api/createRoom'
|
||||
import { InviteDialog } from './InviteDialog'
|
||||
import { type SettingsState } from '@/features/settings'
|
||||
|
||||
export const Conference = ({
|
||||
roomId,
|
||||
@@ -26,19 +26,30 @@ export const Conference = ({
|
||||
mode = 'join',
|
||||
}: {
|
||||
roomId: string
|
||||
userConfig: LocalUserChoices
|
||||
userConfig: {
|
||||
devices: SettingsState['devices']
|
||||
username: SettingsState['username']
|
||||
}
|
||||
mode?: 'join' | 'create'
|
||||
initialRoomData?: ApiRoom
|
||||
}) => {
|
||||
const fetchKey = [keys.room, roomId, userConfig.username]
|
||||
|
||||
const { mutateAsync: createRoom, status: createStatus, isError: isCreateError} = useCreateRoom({
|
||||
const {
|
||||
mutateAsync: createRoom,
|
||||
status: createStatus,
|
||||
isError: isCreateError,
|
||||
} = useCreateRoom({
|
||||
onSuccess: (data) => {
|
||||
queryClient.setQueryData(fetchKey, data)
|
||||
},
|
||||
});
|
||||
})
|
||||
|
||||
const { status: fetchStatus, isError: isFetchError, data } = useQuery({
|
||||
const {
|
||||
status: fetchStatus,
|
||||
isError: isFetchError,
|
||||
data,
|
||||
} = useQuery({
|
||||
queryKey: fetchKey,
|
||||
enabled: !initialRoomData,
|
||||
initialData: initialRoomData,
|
||||
@@ -48,7 +59,7 @@ export const Conference = ({
|
||||
username: userConfig.username,
|
||||
}).catch((error) => {
|
||||
if (error.statusCode == '404') {
|
||||
createRoom({slug: roomId})
|
||||
createRoom({ slug: roomId })
|
||||
}
|
||||
}),
|
||||
retry: false,
|
||||
@@ -57,14 +68,21 @@ export const Conference = ({
|
||||
const roomOptions = useMemo((): RoomOptions => {
|
||||
return {
|
||||
videoCaptureDefaults: {
|
||||
deviceId: userConfig.videoDeviceId ?? undefined,
|
||||
deviceId: userConfig.devices.cameraDeviceId ?? undefined,
|
||||
},
|
||||
audioCaptureDefaults: {
|
||||
deviceId: userConfig.audioDeviceId ?? undefined,
|
||||
deviceId: userConfig.devices.micDeviceId ?? undefined,
|
||||
},
|
||||
audioOutput: {
|
||||
deviceId: userConfig.devices.speakerDeviceId ?? undefined,
|
||||
},
|
||||
}
|
||||
// do not rely on the userConfig object directly as its reference may change on every render
|
||||
}, [userConfig.videoDeviceId, userConfig.audioDeviceId])
|
||||
}, [
|
||||
userConfig.devices.cameraDeviceId,
|
||||
userConfig.devices.micDeviceId,
|
||||
userConfig.devices.speakerDeviceId,
|
||||
])
|
||||
|
||||
const room = useMemo(() => new Room(roomOptions), [roomOptions])
|
||||
|
||||
@@ -92,7 +110,12 @@ export const Conference = ({
|
||||
const { t } = useTranslation('rooms')
|
||||
if (isCreateError) {
|
||||
// this error screen should be replaced by a proper waiting room for anonymous user.
|
||||
return <ErrorScreen title={t('error.createRoom.heading')} body={t('error.createRoom.body')} />
|
||||
return (
|
||||
<ErrorScreen
|
||||
title={t('error.createRoom.heading')}
|
||||
body={t('error.createRoom.body')}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -103,12 +126,10 @@ export const Conference = ({
|
||||
serverUrl={data?.livekit?.url}
|
||||
token={data?.livekit?.token}
|
||||
connect={true}
|
||||
audio={userConfig.audioEnabled}
|
||||
video={userConfig.videoEnabled}
|
||||
audio={userConfig.devices.enableMic}
|
||||
video={userConfig.devices.enableCamera}
|
||||
>
|
||||
<VideoConference
|
||||
chatMessageFormatter={formatChatMessageLinks}
|
||||
/>
|
||||
<VideoConference chatMessageFormatter={formatChatMessageLinks} />
|
||||
{showInviteDialog && (
|
||||
<InviteDialog
|
||||
isOpen={showInviteDialog}
|
||||
|
||||
@@ -0,0 +1,358 @@
|
||||
import { Screen } from '@/layout/Screen'
|
||||
import {
|
||||
Button,
|
||||
Div,
|
||||
Field,
|
||||
Form,
|
||||
H,
|
||||
Menu,
|
||||
MenuList,
|
||||
ToggleButton,
|
||||
VerticallyOffCenter,
|
||||
} from '@/primitives'
|
||||
import { Center, HStack, VStack } from '@/styled-system/jsx'
|
||||
import {
|
||||
RiArrowDropDownLine,
|
||||
RiMicLine,
|
||||
RiMicOffLine,
|
||||
RiVideoOffLine,
|
||||
RiVideoOnLine,
|
||||
RiVolumeUpLine,
|
||||
} from '@remixicon/react'
|
||||
import {
|
||||
useMaybeRoomContext,
|
||||
usePreviewTracks,
|
||||
} from '@livekit/components-react'
|
||||
import { Track, LocalVideoTrack, LocalAudioTrack } from 'livekit-client'
|
||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useSnapshot } from 'valtio'
|
||||
import {
|
||||
usePersistedMediaDeviceSelect,
|
||||
useAudioOutputs,
|
||||
} from '@/features/devices'
|
||||
import { settingsStore, type SettingsState } from '@/features/settings'
|
||||
import { css } from '@/styled-system/css'
|
||||
|
||||
export const HomemadeJoin = ({
|
||||
onSubmit,
|
||||
}: {
|
||||
onSubmit: (choices: {
|
||||
devices: SettingsState['devices']
|
||||
username: SettingsState['username']
|
||||
}) => void
|
||||
}) => {
|
||||
const { t } = useTranslation('rooms', { keyPrefix: 'join' })
|
||||
|
||||
const settingsSnap = useSnapshot(settingsStore)
|
||||
const [initialUserChoices] = useState({ ...settingsSnap.devices })
|
||||
|
||||
const tracks = usePreviewTracks({
|
||||
audio: settingsSnap.devices.enableMic
|
||||
? { deviceId: initialUserChoices.micDeviceId }
|
||||
: false,
|
||||
video: settingsSnap.devices.enableCamera
|
||||
? { deviceId: initialUserChoices.cameraDeviceId }
|
||||
: false,
|
||||
})
|
||||
|
||||
const videoEl = useRef(null)
|
||||
const videoTrack = useMemo(
|
||||
() =>
|
||||
tracks?.filter(
|
||||
(track) => track.kind === Track.Kind.Video
|
||||
)[0] as LocalVideoTrack,
|
||||
[tracks]
|
||||
)
|
||||
|
||||
const audioTrack = useMemo(
|
||||
() =>
|
||||
tracks?.filter(
|
||||
(track) => track.kind === Track.Kind.Audio
|
||||
)[0] as LocalAudioTrack,
|
||||
[tracks]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (videoEl.current && videoTrack) {
|
||||
videoTrack.unmute()
|
||||
videoTrack.attach(videoEl.current)
|
||||
}
|
||||
|
||||
return () => {
|
||||
videoTrack?.detach()
|
||||
}
|
||||
}, [videoTrack])
|
||||
const room = useMaybeRoomContext()
|
||||
|
||||
const {
|
||||
devices: micDevices,
|
||||
activeDeviceId: activeMicDeviceId,
|
||||
setActiveMediaDevice: setActiveMicDevice,
|
||||
} = usePersistedMediaDeviceSelect({
|
||||
kind: 'audioinput',
|
||||
room,
|
||||
track: audioTrack,
|
||||
requestPermissions: true,
|
||||
})
|
||||
const {
|
||||
devices: cameraDevices,
|
||||
activeDeviceId: activeCameraDeviceId,
|
||||
setActiveMediaDevice: setActiveCameraDevice,
|
||||
} = usePersistedMediaDeviceSelect({
|
||||
kind: 'videoinput',
|
||||
room,
|
||||
track: videoTrack,
|
||||
requestPermissions: true,
|
||||
})
|
||||
const speakerDevices = useAudioOutputs()
|
||||
|
||||
useEffect(() => {
|
||||
if (settingsStore.devices.micDeviceId) {
|
||||
setActiveMicDevice(settingsStore.devices.micDeviceId)
|
||||
}
|
||||
if (settingsStore.devices.cameraDeviceId) {
|
||||
setActiveCameraDevice(settingsStore.devices.cameraDeviceId)
|
||||
}
|
||||
}, [setActiveCameraDevice, setActiveMicDevice])
|
||||
|
||||
return (
|
||||
<Screen>
|
||||
<VerticallyOffCenter>
|
||||
<Div
|
||||
className={css({
|
||||
margin: 'auto',
|
||||
flexWrap: 'wrap',
|
||||
width: 'fit-content',
|
||||
maxWidth: 'full',
|
||||
display: 'flex',
|
||||
gap: 2,
|
||||
paddingX: 1,
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
lg: {
|
||||
alignItems: 'stretch',
|
||||
flexDirection: 'row',
|
||||
},
|
||||
})}
|
||||
>
|
||||
<VStack
|
||||
className={css({
|
||||
width: 'full',
|
||||
maxWidth: '38rem',
|
||||
margin: '0 auto',
|
||||
alignItems: 'center',
|
||||
flexShrink: '1',
|
||||
})}
|
||||
>
|
||||
<Center
|
||||
className={css({
|
||||
width: '38rem',
|
||||
maxWidth: 'full',
|
||||
height: 'auto',
|
||||
aspectRatio: '16 / 9',
|
||||
background: 'gray.900',
|
||||
color: 'white',
|
||||
borderRadius: 16,
|
||||
overflow: 'hidden',
|
||||
})}
|
||||
>
|
||||
{videoTrack && settingsSnap.devices.enableCamera ? (
|
||||
// eslint-disable-next-line jsx-a11y/media-has-caption
|
||||
<video
|
||||
ref={videoEl}
|
||||
width="608"
|
||||
height="342"
|
||||
className={css({
|
||||
width: 'full',
|
||||
height: 'auto',
|
||||
})}
|
||||
/>
|
||||
) : (
|
||||
settingsSnap.devices.enableCamera === false && (
|
||||
<p>{t('cameraPlaceholder')}</p>
|
||||
)
|
||||
)}
|
||||
</Center>
|
||||
<HStack gap={1} justify="center" flexWrap={'wrap'}>
|
||||
{/* audio output dropdown */}
|
||||
<Menu>
|
||||
<Button
|
||||
tooltip={t('chooseSpeaker')}
|
||||
aria-label={t('chooseSpeaker')}
|
||||
>
|
||||
<RiVolumeUpLine />
|
||||
<RiArrowDropDownLine />
|
||||
</Button>
|
||||
<MenuList
|
||||
items={speakerDevices.map((d) => ({
|
||||
value: d.deviceId,
|
||||
label: d.label,
|
||||
}))}
|
||||
selectedItem={settingsSnap.devices.speakerDeviceId}
|
||||
onAction={(value) => {
|
||||
settingsStore.devices.speakerDeviceId = value as string
|
||||
}}
|
||||
/>
|
||||
</Menu>
|
||||
|
||||
{/* audio input toggle + dropdown */}
|
||||
<HStack gap={0}>
|
||||
<ToggleButton
|
||||
isSelected={settingsSnap.devices.enableMic}
|
||||
variant={
|
||||
settingsSnap.devices.enableMic ? undefined : 'danger'
|
||||
}
|
||||
toggledStyles={false}
|
||||
onChange={(enabled) =>
|
||||
(settingsStore.devices.enableMic = enabled)
|
||||
}
|
||||
aria-label={
|
||||
settingsSnap.devices.enableMic
|
||||
? `${t('micIsOn')} ${t('toggleOff')}`
|
||||
: `${t('micIsOff')} ${t('toggleOn')}`
|
||||
}
|
||||
tooltip={
|
||||
settingsSnap.devices.enableMic ? (
|
||||
<>
|
||||
{t('micIsOn')}
|
||||
<br />
|
||||
{t('toggleOff')}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{t('micIsOff')}
|
||||
<br />
|
||||
{t('toggleOn')}
|
||||
</>
|
||||
)
|
||||
}
|
||||
groupPosition="left"
|
||||
>
|
||||
{settingsSnap.devices.enableMic ? (
|
||||
<RiMicLine />
|
||||
) : (
|
||||
<RiMicOffLine />
|
||||
)}
|
||||
</ToggleButton>
|
||||
<Menu>
|
||||
<Button
|
||||
tooltip={t('chooseMic')}
|
||||
aria-label={t('chooseMic')}
|
||||
groupPosition="right"
|
||||
square
|
||||
>
|
||||
<RiArrowDropDownLine />
|
||||
</Button>
|
||||
<MenuList
|
||||
items={micDevices.map((d) => ({
|
||||
value: d.deviceId,
|
||||
label: d.label,
|
||||
}))}
|
||||
selectedItem={activeMicDeviceId}
|
||||
onAction={(value) => {
|
||||
setActiveMicDevice(value as string)
|
||||
}}
|
||||
/>
|
||||
</Menu>
|
||||
</HStack>
|
||||
|
||||
{/* video toggle + dropdown */}
|
||||
<HStack gap={0}>
|
||||
<ToggleButton
|
||||
isSelected={settingsSnap.devices.enableCamera}
|
||||
variant={
|
||||
settingsSnap.devices.enableCamera ? undefined : 'danger'
|
||||
}
|
||||
toggledStyles={false}
|
||||
onChange={(enabled) =>
|
||||
(settingsStore.devices.enableCamera = enabled)
|
||||
}
|
||||
aria-label={
|
||||
settingsSnap.devices.enableMic
|
||||
? `${t('cameraIsOn')} ${t('toggleOff')}`
|
||||
: `${t('cameraIsOff')} ${t('toggleOn')}`
|
||||
}
|
||||
tooltip={
|
||||
settingsSnap.devices.enableMic ? (
|
||||
<>
|
||||
{t('cameraIsOn')}
|
||||
<br />
|
||||
{t('toggleOff')}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{t('cameraIsOff')}
|
||||
<br />
|
||||
{t('toggleOn')}
|
||||
</>
|
||||
)
|
||||
}
|
||||
groupPosition="left"
|
||||
>
|
||||
{settingsSnap.devices.enableCamera ? (
|
||||
<RiVideoOnLine />
|
||||
) : (
|
||||
<RiVideoOffLine />
|
||||
)}
|
||||
</ToggleButton>
|
||||
<Menu>
|
||||
<Button
|
||||
tooltip={t('chooseCamera')}
|
||||
aria-label={t('chooseCamera')}
|
||||
groupPosition="right"
|
||||
square
|
||||
>
|
||||
<RiArrowDropDownLine />
|
||||
</Button>
|
||||
<MenuList
|
||||
items={cameraDevices.map((d) => ({
|
||||
value: d.deviceId,
|
||||
label: d.label,
|
||||
}))}
|
||||
selectedItem={activeCameraDeviceId}
|
||||
onAction={(value) => {
|
||||
setActiveCameraDevice(value as string)
|
||||
}}
|
||||
/>
|
||||
</Menu>
|
||||
</HStack>
|
||||
</HStack>
|
||||
</VStack>
|
||||
<Div width="24rem" maxWidth="full" flexShrink="1">
|
||||
<VerticallyOffCenter>
|
||||
<Center>
|
||||
<H lvl={1}>{t('heading')}</H>
|
||||
</Center>
|
||||
<Form
|
||||
onSubmit={(data) => {
|
||||
settingsStore.username = (data.username as string).trim()
|
||||
onSubmit({
|
||||
devices: { ...settingsStore.devices },
|
||||
username: settingsStore.username,
|
||||
})
|
||||
}}
|
||||
submitLabel={t('joinMeeting')}
|
||||
withSubmitButton={false}
|
||||
>
|
||||
<Field
|
||||
type="text"
|
||||
name="username"
|
||||
defaultValue={settingsSnap.username}
|
||||
label={t('usernameLabel')}
|
||||
description={t('usernameHint')}
|
||||
isRequired
|
||||
/>
|
||||
<Center>
|
||||
<Button type="submit" variant="primary">
|
||||
{t('joinMeeting')}
|
||||
</Button>
|
||||
</Center>
|
||||
</Form>
|
||||
</VerticallyOffCenter>
|
||||
</Div>
|
||||
</Div>
|
||||
</VerticallyOffCenter>
|
||||
</Screen>
|
||||
)
|
||||
}
|
||||
@@ -8,7 +8,9 @@ export const FeedbackRoute = () => {
|
||||
return (
|
||||
<Screen layout="centered">
|
||||
<CenteredContent title={t('feedback.heading')} withBackButton>
|
||||
<Text as="p" variant="h3" centered>{t('feedback.body')}</Text>
|
||||
<Text as="p" variant="h3" centered>
|
||||
{t('feedback.body')}
|
||||
</Text>
|
||||
</CenteredContent>
|
||||
</Screen>
|
||||
)
|
||||
|
||||
@@ -1,18 +1,23 @@
|
||||
import { useState } from 'react'
|
||||
import {
|
||||
usePersistentUserChoices,
|
||||
type LocalUserChoices,
|
||||
} from '@livekit/components-react'
|
||||
import { useParams } from 'wouter'
|
||||
import { ErrorScreen } from '@/components/ErrorScreen'
|
||||
import { useUser, UserAware } from '@/features/auth'
|
||||
import { Conference } from '../components/Conference'
|
||||
import { Join } from '../components/Join'
|
||||
import { HomemadeJoin } from '../components/HomemadeJoin'
|
||||
import { settingsStore, type SettingsState } from '@/features/settings'
|
||||
import { useSnapshot } from 'valtio'
|
||||
|
||||
export const Room = () => {
|
||||
const { isLoggedIn } = useUser()
|
||||
const { userChoices: existingUserChoices } = usePersistentUserChoices()
|
||||
const [userConfig, setUserConfig] = useState<LocalUserChoices | null>(null)
|
||||
const settingsSnap = useSnapshot(settingsStore)
|
||||
const existingUserConfig = {
|
||||
username: settingsSnap.username,
|
||||
devices: settingsSnap.devices,
|
||||
}
|
||||
const [userConfig, setUserConfig] = useState<null | {
|
||||
username: SettingsState['username']
|
||||
devices: SettingsState['devices']
|
||||
}>(null)
|
||||
|
||||
const { roomId } = useParams()
|
||||
const initialRoomData = history.state?.initialRoomData
|
||||
@@ -26,7 +31,7 @@ export const Room = () => {
|
||||
if (!userConfig && !skipJoinScreen) {
|
||||
return (
|
||||
<UserAware>
|
||||
<Join onSubmit={setUserConfig} />
|
||||
<HomemadeJoin onSubmit={setUserConfig} />
|
||||
</UserAware>
|
||||
)
|
||||
}
|
||||
@@ -38,7 +43,7 @@ export const Room = () => {
|
||||
roomId={roomId}
|
||||
mode={mode}
|
||||
userConfig={{
|
||||
...existingUserChoices,
|
||||
...existingUserConfig,
|
||||
...userConfig,
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -1,2 +1,4 @@
|
||||
export { SettingsButton } from './components/SettingsButton'
|
||||
export { SettingsDialog } from './components/SettingsDialog'
|
||||
export { settingsStore } from './stores/settings'
|
||||
export { type SettingsState } from './stores/settings'
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import { proxy, subscribe } from 'valtio'
|
||||
import { devtools } from 'valtio/utils'
|
||||
|
||||
export type SettingsState = {
|
||||
username: string | undefined
|
||||
devices: {
|
||||
/**
|
||||
* MediaDeviceInfo id
|
||||
*/
|
||||
speakerDeviceId: string | undefined
|
||||
/**
|
||||
* MediaDeviceInfo id
|
||||
*/
|
||||
micDeviceId: string | undefined
|
||||
/**
|
||||
* MediaDeviceInfo id
|
||||
*/
|
||||
cameraDeviceId: string | undefined
|
||||
enableMic: boolean
|
||||
enableCamera: boolean
|
||||
}
|
||||
}
|
||||
|
||||
// sync the valtio store with localstorage data
|
||||
// @TODO: make it easier to have "persisted" stores as we will definitely use it quite often
|
||||
|
||||
const localData = localStorage.getItem('meet.settings')
|
||||
|
||||
export const settingsStore = proxy<SettingsState>(
|
||||
localData
|
||||
? JSON.parse(localData)
|
||||
: {
|
||||
username: undefined,
|
||||
devices: {
|
||||
speakerDeviceId: undefined,
|
||||
micDeviceId: undefined,
|
||||
cameraDeviceId: undefined,
|
||||
enableMic: false,
|
||||
enableCamera: false,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
subscribe(settingsStore, () => {
|
||||
localStorage.setItem('meet.settings', JSON.stringify(settingsStore))
|
||||
})
|
||||
|
||||
if (import.meta.env.DEV) {
|
||||
devtools(settingsStore, { name: 'settings', enabled: true })
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Button, Popover, PopoverList } from '@/primitives'
|
||||
import { useLanguageLabels } from './useLanguageLabels'
|
||||
|
||||
export const LanguageSelector = () => {
|
||||
const { t, i18n } = useTranslation()
|
||||
const { languagesList, currentLanguage } = useLanguageLabels()
|
||||
return (
|
||||
<Popover aria-label={t('languageSelector.popoverLabel')}>
|
||||
<Button
|
||||
aria-label={t('languageSelector.buttonLabel', {
|
||||
currentLanguage: currentLanguage.label,
|
||||
})}
|
||||
size="sm"
|
||||
variant="primary"
|
||||
outline
|
||||
>
|
||||
{i18n.language}
|
||||
</Button>
|
||||
<PopoverList
|
||||
items={languagesList}
|
||||
onAction={(lang) => {
|
||||
i18n.changeLanguage(lang)
|
||||
}}
|
||||
/>
|
||||
</Popover>
|
||||
)
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import { Div, VerticallyOffCenter } from '@/primitives'
|
||||
import type { SystemStyleObject } from '../styled-system/types'
|
||||
import type { SystemStyleObject } from '@/styled-system/types'
|
||||
|
||||
export const Centered = ({
|
||||
width = '38rem',
|
||||
|
||||
@@ -2,11 +2,13 @@ import { Link } from 'wouter'
|
||||
import { css } from '@/styled-system/css'
|
||||
import { Stack } from '@/styled-system/jsx'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { A, Button, Popover, PopoverList, Text } from '@/primitives'
|
||||
import { A, Text, Button } from '@/primitives'
|
||||
import { SettingsButton } from '@/features/settings'
|
||||
import { authUrl, logoutUrl, useUser } from '@/features/auth'
|
||||
import { useMatchesRoute } from '@/navigation/useMatchesRoute'
|
||||
import { Feedback } from '@/components/Feedback'
|
||||
import { Menu } from '@/primitives/Menu'
|
||||
import { MenuList } from '@/primitives/MenuList'
|
||||
|
||||
export const Header = () => {
|
||||
const { t } = useTranslation()
|
||||
@@ -64,7 +66,7 @@ export const Header = () => {
|
||||
<A href={authUrl()}>{t('login')}</A>
|
||||
)}
|
||||
{!!user && (
|
||||
<Popover aria-label={t('logout')}>
|
||||
<Menu>
|
||||
<Button
|
||||
size="sm"
|
||||
invisible
|
||||
@@ -73,15 +75,15 @@ export const Header = () => {
|
||||
>
|
||||
{user.email}
|
||||
</Button>
|
||||
<PopoverList
|
||||
items={[{ key: 'logout', value: 'logout', label: t('logout') }]}
|
||||
<MenuList
|
||||
items={[{ value: 'logout', label: t('logout') }]}
|
||||
onAction={(value) => {
|
||||
if (value === 'logout') {
|
||||
window.location.href = logoutUrl()
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Popover>
|
||||
</Menu>
|
||||
)}
|
||||
<SettingsButton />
|
||||
</Stack>
|
||||
|
||||
@@ -7,13 +7,6 @@
|
||||
"heading": ""
|
||||
},
|
||||
"feedbackAlert": "",
|
||||
"forbidden": {
|
||||
"heading": ""
|
||||
},
|
||||
"languageSelector": {
|
||||
"buttonLabel": "",
|
||||
"popoverLabel": ""
|
||||
},
|
||||
"loading": "",
|
||||
"loggedInUserTooltip": "",
|
||||
"login": "Anmelden",
|
||||
|
||||
@@ -4,11 +4,24 @@
|
||||
"heading": ""
|
||||
},
|
||||
"join": {
|
||||
"cameraIsOff": "",
|
||||
"cameraIsOn": "",
|
||||
"cameraPlaceholder": "",
|
||||
"camlabel": "",
|
||||
"chooseCamera": "",
|
||||
"chooseMic": "",
|
||||
"chooseSpeaker": "",
|
||||
"heading": "",
|
||||
"joinLabel": "",
|
||||
"joinMeeting": "",
|
||||
"micIsOff": "",
|
||||
"micIsOn": "",
|
||||
"micLabel": "",
|
||||
"userLabel": ""
|
||||
"toggleOff": "",
|
||||
"toggleOn": "",
|
||||
"userLabel": "",
|
||||
"usernameHint": "",
|
||||
"usernameLabel": ""
|
||||
},
|
||||
"leaveRoomPrompt": "",
|
||||
"shareDialog": {
|
||||
|
||||
@@ -7,13 +7,6 @@
|
||||
"heading": "An error occured while loading the page"
|
||||
},
|
||||
"feedbackAlert": "Give us feedback",
|
||||
"forbidden": {
|
||||
"heading": "You don't have the permission to view this page"
|
||||
},
|
||||
"languageSelector": {
|
||||
"buttonLabel": "Change language (currently {{currentLanguage}})",
|
||||
"popoverLabel": "Choose language"
|
||||
},
|
||||
"loading": "Loading…",
|
||||
"loggedInUserTooltip": "Logged in as…",
|
||||
"login": "Login",
|
||||
|
||||
@@ -4,11 +4,24 @@
|
||||
"heading": "Help us improve Meet"
|
||||
},
|
||||
"join": {
|
||||
"camlabel": "Camera",
|
||||
"heading": "Join the meeting",
|
||||
"joinLabel": "Join",
|
||||
"micLabel": "Microphone",
|
||||
"userLabel": "Your name"
|
||||
"cameraIsOff": "Camera is off.",
|
||||
"cameraIsOn": "Camera is on.",
|
||||
"cameraPlaceholder": "Turn on the camera to see the preview",
|
||||
"camlabel": "",
|
||||
"chooseCamera": "Select camera",
|
||||
"chooseMic": "Select microphone",
|
||||
"chooseSpeaker": "Select speakers",
|
||||
"heading": "Verify your settings",
|
||||
"joinLabel": "",
|
||||
"joinMeeting": "Join meeting",
|
||||
"micIsOff": "Microphone is off.",
|
||||
"micIsOn": "Microphone is on.",
|
||||
"micLabel": "",
|
||||
"toggleOff": "Click to turn off",
|
||||
"toggleOn": "Click to turn on",
|
||||
"userLabel": "",
|
||||
"usernameHint": "Shown to other participants",
|
||||
"usernameLabel": "Your name"
|
||||
},
|
||||
"leaveRoomPrompt": "This will make you leave the meeting.",
|
||||
"shareDialog": {
|
||||
|
||||
@@ -7,13 +7,6 @@
|
||||
"heading": "Une erreur est survenue lors du chargement de la page"
|
||||
},
|
||||
"feedbackAlert": "Donnez-nous votre avis",
|
||||
"forbidden": {
|
||||
"heading": "Accès interdit"
|
||||
},
|
||||
"languageSelector": {
|
||||
"buttonLabel": "Changer de langue (actuellement {{currentLanguage}})",
|
||||
"popoverLabel": "Choix de la langue"
|
||||
},
|
||||
"loading": "Chargement…",
|
||||
"loggedInUserTooltip": "Connecté en tant que…",
|
||||
"login": "Se connecter",
|
||||
|
||||
@@ -4,11 +4,24 @@
|
||||
"heading": "Aidez-nous à améliorer Meet"
|
||||
},
|
||||
"join": {
|
||||
"camlabel": "Webcam",
|
||||
"heading": "Rejoindre la réunion",
|
||||
"joinLabel": "Rejoindre",
|
||||
"micLabel": "Micro",
|
||||
"userLabel": "Votre nom"
|
||||
"cameraIsOff": "Webcam coupée.",
|
||||
"cameraIsOn": "Webcam activée.",
|
||||
"cameraPlaceholder": "Activez la webcam pour prévisualiser l'affichage",
|
||||
"camlabel": "",
|
||||
"chooseCamera": "Choisir la webcam",
|
||||
"chooseMic": "Choisir le micro",
|
||||
"chooseSpeaker": "Choisir la sortie audio",
|
||||
"heading": "Vérifiez vos paramètres",
|
||||
"joinLabel": "",
|
||||
"joinMeeting": "Rejoindre la réjoindre",
|
||||
"micIsOff": "Micro coupé.",
|
||||
"micIsOn": "Micro activé.",
|
||||
"micLabel": "",
|
||||
"toggleOff": "Cliquez pour désactiver",
|
||||
"toggleOn": "Cliquez pour activer",
|
||||
"userLabel": "",
|
||||
"usernameHint": "Affiché aux autres participants",
|
||||
"usernameLabel": "Votre nom"
|
||||
},
|
||||
"leaveRoomPrompt": "Revenir à l'accueil vous fera quitter la réunion.",
|
||||
"shareDialog": {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { cva } from '@/styled-system/css'
|
||||
import { styled } from '../styled-system/jsx'
|
||||
import { styled } from '@/styled-system/jsx'
|
||||
|
||||
const box = cva({
|
||||
base: {
|
||||
|
||||
@@ -1,125 +1,20 @@
|
||||
import { type ReactNode } from 'react'
|
||||
import {
|
||||
Button as RACButton,
|
||||
type ButtonProps as RACButtonsProps,
|
||||
TooltipTrigger,
|
||||
Link,
|
||||
LinkProps,
|
||||
} from 'react-aria-components'
|
||||
import { cva, type RecipeVariantProps } from '@/styled-system/css'
|
||||
import { Tooltip } from './Tooltip'
|
||||
import { type RecipeVariantProps } from '@/styled-system/css'
|
||||
import { buttonRecipe, type ButtonRecipe } from './buttonRecipe'
|
||||
import { TooltipWrapper, type TooltipWrapperProps } from './TooltipWrapper'
|
||||
|
||||
const button = cva({
|
||||
base: {
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
transition: 'background 200ms, outline 200ms, border-color 200ms',
|
||||
cursor: 'pointer',
|
||||
border: '1px solid transparent',
|
||||
color: 'colorPalette.text',
|
||||
backgroundColor: 'colorPalette',
|
||||
'&[data-hovered]': {
|
||||
backgroundColor: 'colorPalette.hover',
|
||||
},
|
||||
'&[data-pressed]': {
|
||||
backgroundColor: 'colorPalette.active',
|
||||
},
|
||||
},
|
||||
variants: {
|
||||
size: {
|
||||
default: {
|
||||
borderRadius: 8,
|
||||
paddingX: '1',
|
||||
paddingY: '0.625',
|
||||
'--square-padding': '{spacing.0.625}',
|
||||
},
|
||||
sm: {
|
||||
borderRadius: 4,
|
||||
paddingX: '0.5',
|
||||
paddingY: '0.25',
|
||||
'--square-padding': '{spacing.0.25}',
|
||||
},
|
||||
xs: {
|
||||
borderRadius: 4,
|
||||
'--square-padding': '0',
|
||||
},
|
||||
},
|
||||
square: {
|
||||
true: {
|
||||
paddingX: 'var(--square-padding)',
|
||||
paddingY: 'var(--square-padding)',
|
||||
},
|
||||
},
|
||||
variant: {
|
||||
default: {
|
||||
colorPalette: 'control',
|
||||
},
|
||||
primary: {
|
||||
colorPalette: 'primary',
|
||||
},
|
||||
// @TODO: better handling of colors… this is a mess
|
||||
success: {
|
||||
colorPalette: 'success',
|
||||
borderColor: 'success.300',
|
||||
color: 'success.subtle-text',
|
||||
backgroundColor: 'success.subtle',
|
||||
'&[data-hovered]': {
|
||||
backgroundColor: 'success.200',
|
||||
},
|
||||
'&[data-pressed]': {
|
||||
backgroundColor: 'success.subtle!',
|
||||
},
|
||||
},
|
||||
},
|
||||
outline: {
|
||||
true: {
|
||||
color: 'colorPalette',
|
||||
backgroundColor: 'transparent!',
|
||||
borderColor: 'currentcolor!',
|
||||
'&[data-hovered]': {
|
||||
backgroundColor: 'colorPalette.subtle!',
|
||||
},
|
||||
'&[data-pressed]': {
|
||||
backgroundColor: 'colorPalette.subtle!',
|
||||
},
|
||||
},
|
||||
},
|
||||
invisible: {
|
||||
true: {
|
||||
borderColor: 'none!',
|
||||
backgroundColor: 'none!',
|
||||
'&[data-hovered]': {
|
||||
backgroundColor: 'none!',
|
||||
borderColor: 'colorPalette.active!',
|
||||
},
|
||||
'&[data-pressed]': {
|
||||
borderColor: 'currentcolor',
|
||||
},
|
||||
},
|
||||
},
|
||||
fullWidth: {
|
||||
true: {
|
||||
width: 'full',
|
||||
},
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
size: 'default',
|
||||
variant: 'default',
|
||||
outline: false,
|
||||
},
|
||||
})
|
||||
|
||||
type Tooltip = {
|
||||
tooltip?: string
|
||||
tooltipType?: 'instant' | 'delayed'
|
||||
}
|
||||
export type ButtonProps = RecipeVariantProps<typeof button> &
|
||||
export type ButtonProps = RecipeVariantProps<ButtonRecipe> &
|
||||
RACButtonsProps &
|
||||
Tooltip
|
||||
TooltipWrapperProps
|
||||
|
||||
type LinkButtonProps = RecipeVariantProps<typeof button> & LinkProps & Tooltip
|
||||
type LinkButtonProps = RecipeVariantProps<ButtonRecipe> &
|
||||
LinkProps &
|
||||
TooltipWrapperProps
|
||||
|
||||
type ButtonOrLinkProps = ButtonProps | LinkButtonProps
|
||||
|
||||
@@ -128,37 +23,20 @@ export const Button = ({
|
||||
tooltipType = 'instant',
|
||||
...props
|
||||
}: ButtonOrLinkProps) => {
|
||||
const [variantProps, componentProps] = button.splitVariantProps(props)
|
||||
const [variantProps, componentProps] = buttonRecipe.splitVariantProps(props)
|
||||
if ((props as LinkButtonProps).href !== undefined) {
|
||||
return (
|
||||
<TooltipWrapper tooltip={tooltip} tooltipType={tooltipType}>
|
||||
<Link className={button(variantProps)} {...componentProps} />
|
||||
<Link className={buttonRecipe(variantProps)} {...componentProps} />
|
||||
</TooltipWrapper>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<TooltipWrapper tooltip={tooltip} tooltipType={tooltipType}>
|
||||
<RACButton
|
||||
className={button(variantProps)}
|
||||
className={buttonRecipe(variantProps)}
|
||||
{...(componentProps as RACButtonsProps)}
|
||||
/>
|
||||
</TooltipWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
const TooltipWrapper = ({
|
||||
tooltip,
|
||||
tooltipType,
|
||||
children,
|
||||
}: {
|
||||
children: ReactNode
|
||||
} & Tooltip) => {
|
||||
return tooltip ? (
|
||||
<TooltipTrigger delay={tooltipType === 'instant' ? 300 : 1000}>
|
||||
{children}
|
||||
<Tooltip>{tooltip}</Tooltip>
|
||||
</TooltipTrigger>
|
||||
) : (
|
||||
children
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import { ReactNode, useContext } from 'react'
|
||||
import { OverlayTriggerStateContext } from 'react-aria-components'
|
||||
|
||||
/**
|
||||
* Small helper you can use as a wrapper of a <Dialog> component if you want to make sure it is not rendered when it is closed.
|
||||
*
|
||||
* Not required all the time, it's mostly helpful to avoid calling hooks of a child component that uses a Dialog.
|
||||
*/
|
||||
export const DialogContainer = ({ children }: { children: ReactNode }) => {
|
||||
const state = useContext(OverlayTriggerStateContext)!
|
||||
return state.isOpen ? children : null
|
||||
}
|
||||
@@ -13,6 +13,7 @@ import { Button, useCloseDialog } from '@/primitives'
|
||||
export const Form = ({
|
||||
onSubmit,
|
||||
submitLabel,
|
||||
withSubmitButton = true,
|
||||
withCancelButton = true,
|
||||
onCancelButtonPress,
|
||||
children,
|
||||
@@ -25,6 +26,7 @@ export const Form = ({
|
||||
event: FormEvent<HTMLFormElement>
|
||||
) => void
|
||||
submitLabel: string
|
||||
withSubmitButton?: boolean
|
||||
withCancelButton?: boolean
|
||||
onCancelButtonPress?: () => void
|
||||
}) => {
|
||||
@@ -46,16 +48,18 @@ export const Form = ({
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
<HStack gap="gutter">
|
||||
<Button type="submit" variant="primary">
|
||||
{submitLabel}
|
||||
</Button>
|
||||
{!!onCancel && (
|
||||
<Button variant="primary" outline onPress={() => onCancel()}>
|
||||
{t('cancel')}
|
||||
{withSubmitButton && (
|
||||
<HStack gap="gutter">
|
||||
<Button type="submit" variant="primary">
|
||||
{submitLabel}
|
||||
</Button>
|
||||
)}
|
||||
</HStack>
|
||||
{!!onCancel && (
|
||||
<Button variant="primary" outline onPress={() => onCancel()}>
|
||||
{t('cancel')}
|
||||
</Button>
|
||||
)}
|
||||
</HStack>
|
||||
)}
|
||||
</RACForm>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import { ReactNode } from 'react'
|
||||
import { MenuTrigger } from 'react-aria-components'
|
||||
import { StyledPopover } from './Popover'
|
||||
import { Box } from './Box'
|
||||
|
||||
/**
|
||||
* a Menu is a tuple of a trigger component (most usually a Button) that toggles menu items in a tooltip around the trigger
|
||||
*/
|
||||
export const Menu = ({
|
||||
children,
|
||||
}: {
|
||||
children: [trigger: ReactNode, menu: ReactNode]
|
||||
}) => {
|
||||
const [trigger, menu] = children
|
||||
return (
|
||||
<MenuTrigger>
|
||||
{trigger}
|
||||
<StyledPopover>
|
||||
<Box size="sm" type="popover">
|
||||
{menu}
|
||||
</Box>
|
||||
</StyledPopover>
|
||||
</MenuTrigger>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import { ReactNode } from 'react'
|
||||
import { Menu, MenuProps, MenuItem } from 'react-aria-components'
|
||||
import { menuItemRecipe } from './menuItemRecipe'
|
||||
|
||||
/**
|
||||
* render a Button primitive that shows a popover showing a list of pressable items
|
||||
*/
|
||||
export const MenuList = <T extends string | number = string>({
|
||||
onAction,
|
||||
selectedItem,
|
||||
items = [],
|
||||
...menuProps
|
||||
}: {
|
||||
onAction: (key: T) => void
|
||||
selectedItem?: T
|
||||
items: Array<string | { value: T; label: ReactNode }>
|
||||
} & MenuProps<unknown>) => {
|
||||
return (
|
||||
<Menu
|
||||
selectionMode={selectedItem !== undefined ? 'single' : undefined}
|
||||
selectedKeys={selectedItem !== undefined ? [selectedItem] : undefined}
|
||||
{...menuProps}
|
||||
>
|
||||
{items.map((item) => {
|
||||
const value = typeof item === 'string' ? item : item.value
|
||||
const label = typeof item === 'string' ? item : item.label
|
||||
return (
|
||||
<MenuItem
|
||||
className={menuItemRecipe()}
|
||||
key={value}
|
||||
id={value as string}
|
||||
onAction={() => {
|
||||
onAction(value as T)
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
</MenuItem>
|
||||
)
|
||||
})}
|
||||
</Menu>
|
||||
)
|
||||
}
|
||||
@@ -49,7 +49,10 @@ const StyledOverlayArrow = styled(OverlayArrow, {
|
||||
})
|
||||
|
||||
/**
|
||||
* a Popover is a tuple of a trigger component (most usually a Button) that toggles some interactive content in a tooltip around the trigger
|
||||
* a Popover is a tuple of a trigger component (most usually a Button) that toggles some content in a tooltip around the trigger
|
||||
*
|
||||
* Note: to show a list of actionable items, like a dropdown menu, prefer using a <Menu> or <Select>.
|
||||
* This is here when needing to show unrestricted content in a box.
|
||||
*/
|
||||
export const Popover = ({
|
||||
children,
|
||||
|
||||
@@ -1,72 +0,0 @@
|
||||
import { ReactNode, useContext } from 'react'
|
||||
import {
|
||||
ButtonProps,
|
||||
Button,
|
||||
OverlayTriggerStateContext,
|
||||
} from 'react-aria-components'
|
||||
import { styled } from '@/styled-system/jsx'
|
||||
|
||||
const ListItem = styled(Button, {
|
||||
base: {
|
||||
paddingY: 0.125,
|
||||
paddingX: 0.5,
|
||||
textAlign: 'left',
|
||||
width: 'full',
|
||||
borderRadius: 4,
|
||||
cursor: 'pointer',
|
||||
color: 'box.text',
|
||||
border: '1px solid transparent',
|
||||
'&[data-selected]': {
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
'&[data-focused]': {
|
||||
color: 'primary.text',
|
||||
backgroundColor: 'primary',
|
||||
outline: 'none!',
|
||||
},
|
||||
'&[data-hovered]': {
|
||||
color: 'primary.text',
|
||||
backgroundColor: 'primary',
|
||||
outline: 'none!',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
/**
|
||||
* render a Button primitive that shows a popover showing a list of pressable items
|
||||
*/
|
||||
export const PopoverList = <T extends string | number = string>({
|
||||
onAction,
|
||||
closeOnAction = true,
|
||||
items = [],
|
||||
}: {
|
||||
closeOnAction?: boolean
|
||||
onAction: (key: T) => void
|
||||
items: Array<string | { key: string, value: T; label: ReactNode }>
|
||||
} & ButtonProps) => {
|
||||
const popoverState = useContext(OverlayTriggerStateContext)!
|
||||
return (
|
||||
<ul>
|
||||
{items.map((item) => {
|
||||
const value = typeof item === 'string' ? item : item.value
|
||||
const label = typeof item === 'string' ? item : item.label
|
||||
const key = typeof item === 'string' ? item : item.key
|
||||
return (
|
||||
<li key={key}>
|
||||
<ListItem
|
||||
key={value}
|
||||
onPress={() => {
|
||||
onAction(value as T)
|
||||
if (closeOnAction) {
|
||||
popoverState.close()
|
||||
}
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
</ListItem>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
)
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
} from 'react-aria-components'
|
||||
import { Box } from './Box'
|
||||
import { StyledPopover } from './Popover'
|
||||
import { menuItemRecipe } from './menuItemRecipe'
|
||||
|
||||
const StyledButton = styled(Button, {
|
||||
base: {
|
||||
@@ -43,27 +44,6 @@ const StyledSelectValue = styled(SelectValue, {
|
||||
},
|
||||
})
|
||||
|
||||
const StyledListBoxItem = styled(ListBoxItem, {
|
||||
base: {
|
||||
paddingY: 0.125,
|
||||
paddingX: 0.5,
|
||||
textAlign: 'left',
|
||||
width: 'full',
|
||||
borderRadius: 4,
|
||||
cursor: 'pointer',
|
||||
color: 'box.text',
|
||||
border: '1px solid transparent',
|
||||
'&[data-selected]': {
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
'&[data-focused]': {
|
||||
color: 'primary.text',
|
||||
backgroundColor: 'primary',
|
||||
outline: 'none!',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
export const Select = <T extends string | number>({
|
||||
label,
|
||||
items,
|
||||
@@ -85,9 +65,13 @@ export const Select = <T extends string | number>({
|
||||
<Box size="sm" type="popover" variant="control">
|
||||
<ListBox>
|
||||
{items.map((item) => (
|
||||
<StyledListBoxItem id={item.value} key={item.value}>
|
||||
<ListBoxItem
|
||||
className={menuItemRecipe()}
|
||||
id={item.value}
|
||||
key={item.value}
|
||||
>
|
||||
{item.label}
|
||||
</StyledListBoxItem>
|
||||
</ListBoxItem>
|
||||
))}
|
||||
</ListBox>
|
||||
</Box>
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import {
|
||||
ToggleButton as RACToggleButton,
|
||||
ToggleButtonProps,
|
||||
} from 'react-aria-components'
|
||||
import { type ButtonRecipeProps, buttonRecipe } from './buttonRecipe'
|
||||
import { TooltipWrapper, TooltipWrapperProps } from './TooltipWrapper'
|
||||
|
||||
/**
|
||||
* React aria ToggleButton with our button styles, that can take a tooltip if needed
|
||||
*/
|
||||
export const ToggleButton = ({
|
||||
tooltip,
|
||||
tooltipType,
|
||||
...props
|
||||
}: ToggleButtonProps & ButtonRecipeProps & TooltipWrapperProps) => {
|
||||
const [variantProps, componentProps] = buttonRecipe.splitVariantProps(props)
|
||||
return (
|
||||
<TooltipWrapper tooltip={tooltip} tooltipType={tooltipType}>
|
||||
<RACToggleButton
|
||||
{...componentProps}
|
||||
className={buttonRecipe(variantProps)}
|
||||
/>
|
||||
</TooltipWrapper>
|
||||
)
|
||||
}
|
||||
+30
-5
@@ -2,16 +2,41 @@ import { type ReactNode } from 'react'
|
||||
import {
|
||||
OverlayArrow,
|
||||
Tooltip as RACTooltip,
|
||||
TooltipProps,
|
||||
TooltipTrigger,
|
||||
type TooltipProps,
|
||||
} from 'react-aria-components'
|
||||
import { styled } from '@/styled-system/jsx'
|
||||
|
||||
export type TooltipWrapperProps = {
|
||||
tooltip?: ReactNode
|
||||
tooltipType?: 'instant' | 'delayed'
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrap a component you want to apply a tooltip on (for example a Button)
|
||||
*
|
||||
* If no tooltip is given, just returns children
|
||||
*/
|
||||
export const TooltipWrapper = ({
|
||||
tooltip,
|
||||
tooltipType,
|
||||
children,
|
||||
}: {
|
||||
children: ReactNode
|
||||
} & TooltipWrapperProps) => {
|
||||
return tooltip ? (
|
||||
<TooltipTrigger delay={tooltipType === 'instant' ? 300 : 1000}>
|
||||
{children}
|
||||
<Tooltip>{tooltip}</Tooltip>
|
||||
</TooltipTrigger>
|
||||
) : (
|
||||
children
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Styled react aria Tooltip component.
|
||||
*
|
||||
* Note that tooltips are directly handled by Buttons via the `tooltip` prop,
|
||||
* so you should not need to use this component directly.
|
||||
*
|
||||
* Style taken from example at https://react-spectrum.adobe.com/react-aria/Tooltip.html
|
||||
*/
|
||||
const StyledTooltip = styled(RACTooltip, {
|
||||
@@ -80,7 +105,7 @@ const TooltipArrow = () => {
|
||||
)
|
||||
}
|
||||
|
||||
export const Tooltip = ({
|
||||
const Tooltip = ({
|
||||
children,
|
||||
...props
|
||||
}: Omit<TooltipProps, 'children'> & { children: ReactNode }) => {
|
||||
@@ -1,4 +1,4 @@
|
||||
import { styled } from '../styled-system/jsx'
|
||||
import { styled } from '@/styled-system/jsx'
|
||||
|
||||
export const Ul = styled('ul', {
|
||||
base: {
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
import { type RecipeVariantProps, cva } from '@/styled-system/css'
|
||||
|
||||
export type ButtonRecipe = typeof buttonRecipe
|
||||
|
||||
export type ButtonRecipeProps = RecipeVariantProps<ButtonRecipe>
|
||||
|
||||
export const buttonRecipe = cva({
|
||||
base: {
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
transition: 'background 200ms, outline 200ms, border-color 200ms',
|
||||
cursor: 'pointer',
|
||||
border: '1px solid transparent',
|
||||
color: 'colorPalette.text',
|
||||
backgroundColor: 'colorPalette',
|
||||
'&[data-hovered]': {
|
||||
backgroundColor: 'colorPalette.hover',
|
||||
},
|
||||
'&[data-pressed]': {
|
||||
backgroundColor: 'colorPalette.active',
|
||||
},
|
||||
'&[data-selected]': {
|
||||
backgroundColor: 'colorPalette.active',
|
||||
},
|
||||
},
|
||||
variants: {
|
||||
size: {
|
||||
default: {
|
||||
borderRadius: 8,
|
||||
paddingX: '1',
|
||||
paddingY: '0.625',
|
||||
'--square-padding': '{spacing.0.625}',
|
||||
},
|
||||
sm: {
|
||||
borderRadius: 4,
|
||||
paddingX: '0.5',
|
||||
paddingY: '0.25',
|
||||
'--square-padding': '{spacing.0.25}',
|
||||
},
|
||||
xs: {
|
||||
borderRadius: 4,
|
||||
'--square-padding': '0',
|
||||
},
|
||||
},
|
||||
square: {
|
||||
true: {
|
||||
paddingX: 'var(--square-padding)',
|
||||
paddingY: 'var(--square-padding)',
|
||||
},
|
||||
},
|
||||
variant: {
|
||||
default: {
|
||||
colorPalette: 'control',
|
||||
borderColor: 'control.subtle',
|
||||
},
|
||||
primary: {
|
||||
colorPalette: 'primary',
|
||||
},
|
||||
// @TODO: better handling of colors… this is a mess
|
||||
success: {
|
||||
colorPalette: 'success',
|
||||
borderColor: 'success.300',
|
||||
color: 'success.subtle-text',
|
||||
backgroundColor: 'success.subtle',
|
||||
'&[data-hovered]': {
|
||||
backgroundColor: 'success.200',
|
||||
},
|
||||
'&[data-pressed]': {
|
||||
backgroundColor: 'success.subtle!',
|
||||
},
|
||||
},
|
||||
danger: {
|
||||
colorPalette: 'danger',
|
||||
borderColor: 'danger.600',
|
||||
color: 'danger.subtle-text',
|
||||
backgroundColor: 'danger.subtle',
|
||||
'&[data-hovered]': {
|
||||
backgroundColor: 'danger.200',
|
||||
},
|
||||
'&[data-pressed]': {
|
||||
backgroundColor: 'danger.subtle!',
|
||||
},
|
||||
},
|
||||
},
|
||||
outline: {
|
||||
true: {
|
||||
color: 'colorPalette',
|
||||
backgroundColor: 'transparent!',
|
||||
borderColor: 'currentcolor!',
|
||||
'&[data-hovered]': {
|
||||
backgroundColor: 'colorPalette.subtle!',
|
||||
},
|
||||
'&[data-pressed]': {
|
||||
backgroundColor: 'colorPalette.subtle!',
|
||||
},
|
||||
},
|
||||
},
|
||||
invisible: {
|
||||
true: {
|
||||
borderColor: 'none!',
|
||||
backgroundColor: 'none!',
|
||||
'&[data-hovered]': {
|
||||
backgroundColor: 'none!',
|
||||
borderColor: 'colorPalette.active!',
|
||||
},
|
||||
'&[data-pressed]': {
|
||||
borderColor: 'currentcolor',
|
||||
},
|
||||
},
|
||||
},
|
||||
fullWidth: {
|
||||
true: {
|
||||
width: 'full',
|
||||
},
|
||||
},
|
||||
/**
|
||||
* if the button is next to other ones to make a "button group", tell where the button is to handle radius
|
||||
*/
|
||||
groupPosition: {
|
||||
left: {
|
||||
borderTopRightRadius: 0,
|
||||
borderBottomRightRadius: 0,
|
||||
},
|
||||
right: {
|
||||
borderTopLeftRadius: 0,
|
||||
borderBottomLeftRadius: 0,
|
||||
borderLeft: 0,
|
||||
},
|
||||
center: {
|
||||
borderRadius: 0,
|
||||
},
|
||||
},
|
||||
// some toggle buttons make more sense without a "pushed button" style when enabled because their content changes to mark the state
|
||||
toggledStyles: {
|
||||
false: {
|
||||
'&[data-selected]': {
|
||||
backgroundColor: 'colorPalette',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
size: 'default',
|
||||
variant: 'default',
|
||||
outline: false,
|
||||
toggledStyles: true,
|
||||
},
|
||||
})
|
||||
@@ -1,3 +1,9 @@
|
||||
/**
|
||||
* exposes all primitives we want to use in other parts of the app.
|
||||
*
|
||||
* It's intended not everything is exported: some primitives are meant as building-blocks
|
||||
* for other primitives and don't have any value being exposed.
|
||||
*/
|
||||
export { A } from './A'
|
||||
export { Badge } from './Badge'
|
||||
export { Bold } from './Bold'
|
||||
@@ -5,6 +11,7 @@ export { Box } from './Box'
|
||||
export { Button } from './Button'
|
||||
export { useCloseDialog } from './useCloseDialog'
|
||||
export { Dialog, type DialogProps } from './Dialog'
|
||||
export { DialogContainer } from './DialogContainer'
|
||||
export { Div } from './Div'
|
||||
export { Field } from './Field'
|
||||
export { Form } from './Form'
|
||||
@@ -13,9 +20,11 @@ export { Hr } from './Hr'
|
||||
export { Italic } from './Italic'
|
||||
export { Input } from './Input'
|
||||
export { Link } from './Link'
|
||||
export { Menu } from './Menu'
|
||||
export { MenuList } from './MenuList'
|
||||
export { P } from './P'
|
||||
export { Popover } from './Popover'
|
||||
export { PopoverList } from './PopoverList'
|
||||
export { Text } from './Text'
|
||||
export { ToggleButton } from './ToggleButton'
|
||||
export { Ul } from './Ul'
|
||||
export { VerticallyOffCenter } from './VerticallyOffCenter'
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import { cva } from '@/styled-system/css'
|
||||
|
||||
/**
|
||||
* reusable styles for a menu item, select item, etc… to be used with panda `css()` or `styled()`
|
||||
*
|
||||
* these are in their own files because react hot refresh doesn't like exporting stuff
|
||||
* that aren't components in component files
|
||||
*/
|
||||
export const menuItemRecipe = cva({
|
||||
base: {
|
||||
paddingY: 0.125,
|
||||
paddingX: 0.5,
|
||||
paddingLeft: 1.5,
|
||||
textAlign: 'left',
|
||||
width: 'full',
|
||||
borderRadius: 4,
|
||||
cursor: 'pointer',
|
||||
color: 'box.text',
|
||||
border: '1px solid transparent',
|
||||
position: 'relative',
|
||||
'&[data-selected]': {
|
||||
'&::before': {
|
||||
content: '"✓"',
|
||||
position: 'absolute',
|
||||
top: '2px',
|
||||
left: '6px',
|
||||
},
|
||||
},
|
||||
'&[data-focused]': {
|
||||
color: 'primary.text',
|
||||
backgroundColor: 'primary',
|
||||
outline: 'none!',
|
||||
},
|
||||
'&[data-hovered]': {
|
||||
color: 'primary.text',
|
||||
backgroundColor: 'primary',
|
||||
outline: 'none!',
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -1,6 +1,5 @@
|
||||
import { LogLevel, setLogLevel } from "livekit-client";
|
||||
|
||||
import { LogLevel, setLogLevel } from 'livekit-client'
|
||||
|
||||
export const silenceLiveKitLogs = (shouldSilenceLogs: boolean) => {
|
||||
setLogLevel(shouldSilenceLogs ? LogLevel.silent : LogLevel.debug);
|
||||
setLogLevel(shouldSilenceLogs ? LogLevel.silent : LogLevel.debug)
|
||||
}
|
||||
|
||||
Generated
+1705
File diff suppressed because it is too large
Load Diff
@@ -11,7 +11,7 @@
|
||||
"scripts": {
|
||||
"build-mjml-to-html": "bash ./bin/mjml-to-html",
|
||||
"build-html-to-plain-text": "bash ./bin/html-to-plain-text",
|
||||
"build": "yarn build-mjml-to-html && yarn build-html-to-plain-text"
|
||||
"build": "npm run build-mjml-to-html && npm run build-html-to-plain-text"
|
||||
},
|
||||
"volta": {
|
||||
"node": "16.15.1"
|
||||
|
||||
-1292
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user