mirror of
https://github.com/dergigi/boris.git
synced 2026-02-23 07:54:59 +01:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7de8c49b01 | ||
|
|
c3aece1722 | ||
|
|
7a4cb77aa3 | ||
|
|
9065501043 | ||
|
|
c9ace72d4d | ||
|
|
be6ad79f60 | ||
|
|
0473ba71fb | ||
|
|
7e575ea617 | ||
|
|
c3a2dd5603 |
22
README.md
22
README.md
@@ -2,7 +2,27 @@
|
|||||||
|
|
||||||
Your reading list for the Nostr world.
|
Your reading list for the Nostr world.
|
||||||
|
|
||||||
Boris turns your Nostr bookmarks into a calm, fast, and focused reading experience. Connect your Nostr account and you’ll get a clean three‑pane reader: bookmarks on the left, the article in the middle, and highlights on the right.
|
Boris turns your Nostr bookmarks into a calm, fast, and focused reading experience. Connect your Nostr account and you'll get a clean three‑pane reader: bookmarks on the left, the article in the middle, and highlights on the right.
|
||||||
|
|
||||||
|
## The Vision
|
||||||
|
|
||||||
|
When I wrote "Purple Text, Orange Highlights" 2.5 years ago, I had a certain interface in mind that would allow the reader to curate, discover, highlight, and provide value to writers and other readers alike. Boris is my attempt to build this interface.
|
||||||
|
|
||||||
|
Boris has three "levels" of highlights for each article:
|
||||||
|
- user = yellow
|
||||||
|
- friends = orange
|
||||||
|
- nostrverse = purple
|
||||||
|
|
||||||
|
In case it's not self-explanatory:
|
||||||
|
- **your highlights** = highlights that the logged-in npub made
|
||||||
|
- **friends** = highlights that your friends made, i.e. highlights of the npubs that the logged-in user follows
|
||||||
|
- **nostrverse** = all the highlights we can find on all the relays we're connected to
|
||||||
|
|
||||||
|
The user can toggle hide/show any of these "levels".
|
||||||
|
|
||||||
|
In addition to rendering articles from nostr and the legacy web, Boris can act as a "read it later" app, thanks to the power of nostr bookmarks.
|
||||||
|
|
||||||
|
If you bookmark something on nostr, Boris will show it in the bookmarks bar. If said something contains a URL, Boris will extract and render it in a distraction-free and reader-friendly way.
|
||||||
|
|
||||||
## What Boris does
|
## What Boris does
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "boris",
|
"name": "boris",
|
||||||
"version": "0.2.0",
|
"version": "0.2.1",
|
||||||
"description": "A minimal nostr client for bookmark management",
|
"description": "A minimal nostr client for bookmark management",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
189
src/App.tsx
189
src/App.tsx
@@ -2,13 +2,12 @@ import { useState, useEffect } from 'react'
|
|||||||
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'
|
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||||
import { faSpinner } from '@fortawesome/free-solid-svg-icons'
|
import { faSpinner } from '@fortawesome/free-solid-svg-icons'
|
||||||
import { EventStoreProvider, AccountsProvider } from 'applesauce-react'
|
import { EventStoreProvider, AccountsProvider, Hooks } from 'applesauce-react'
|
||||||
import { EventStore } from 'applesauce-core'
|
import { EventStore } from 'applesauce-core'
|
||||||
import { AccountManager } from 'applesauce-accounts'
|
import { AccountManager } from 'applesauce-accounts'
|
||||||
import { registerCommonAccountTypes } from 'applesauce-accounts/accounts'
|
import { registerCommonAccountTypes } from 'applesauce-accounts/accounts'
|
||||||
import { RelayPool } from 'applesauce-relay'
|
import { RelayPool } from 'applesauce-relay'
|
||||||
import { createAddressLoader } from 'applesauce-loaders/loaders'
|
import { createAddressLoader } from 'applesauce-loaders/loaders'
|
||||||
import Login from './components/Login'
|
|
||||||
import Bookmarks from './components/Bookmarks'
|
import Bookmarks from './components/Bookmarks'
|
||||||
import Toast from './components/Toast'
|
import Toast from './components/Toast'
|
||||||
import { useToast } from './hooks/useToast'
|
import { useToast } from './hooks/useToast'
|
||||||
@@ -16,6 +15,38 @@ import { useToast } from './hooks/useToast'
|
|||||||
const DEFAULT_ARTICLE = import.meta.env.VITE_DEFAULT_ARTICLE_NADDR ||
|
const DEFAULT_ARTICLE = import.meta.env.VITE_DEFAULT_ARTICLE_NADDR ||
|
||||||
'naddr1qvzqqqr4gupzqmjxss3dld622uu8q25gywum9qtg4w4cv4064jmg20xsac2aam5nqqxnzd3cxqmrzv3exgmr2wfesgsmew'
|
'naddr1qvzqqqr4gupzqmjxss3dld622uu8q25gywum9qtg4w4cv4064jmg20xsac2aam5nqqxnzd3cxqmrzv3exgmr2wfesgsmew'
|
||||||
|
|
||||||
|
// AppRoutes component that has access to hooks
|
||||||
|
function AppRoutes({
|
||||||
|
relayPool,
|
||||||
|
showToast
|
||||||
|
}: {
|
||||||
|
relayPool: RelayPool
|
||||||
|
showToast: (message: string) => void
|
||||||
|
}) {
|
||||||
|
const accountManager = Hooks.useAccountManager()
|
||||||
|
|
||||||
|
const handleLogout = () => {
|
||||||
|
accountManager.setActive(undefined as never)
|
||||||
|
localStorage.removeItem('active')
|
||||||
|
showToast('Logged out successfully')
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Routes>
|
||||||
|
<Route
|
||||||
|
path="/a/:naddr"
|
||||||
|
element={
|
||||||
|
<Bookmarks
|
||||||
|
relayPool={relayPool}
|
||||||
|
onLogout={handleLogout}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Route path="/" element={<Navigate to={`/a/${DEFAULT_ARTICLE}`} replace />} />
|
||||||
|
</Routes>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const [eventStore, setEventStore] = useState<EventStore | null>(null)
|
const [eventStore, setEventStore] = useState<EventStore | null>(null)
|
||||||
const [accountManager, setAccountManager] = useState<AccountManager | null>(null)
|
const [accountManager, setAccountManager] = useState<AccountManager | null>(null)
|
||||||
@@ -23,15 +54,15 @@ function App() {
|
|||||||
const { toastMessage, toastType, showToast, clearToast } = useToast()
|
const { toastMessage, toastType, showToast, clearToast } = useToast()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Initialize event store, account manager, and relay pool
|
const initializeApp = async () => {
|
||||||
const store = new EventStore()
|
// Initialize event store, account manager, and relay pool
|
||||||
const accounts = new AccountManager()
|
const store = new EventStore()
|
||||||
|
const accounts = new AccountManager()
|
||||||
// Register common account types (needed for deserialization)
|
|
||||||
registerCommonAccountTypes(accounts)
|
// Register common account types (needed for deserialization)
|
||||||
|
registerCommonAccountTypes(accounts)
|
||||||
// Load persisted accounts from localStorage
|
|
||||||
const loadAccounts = async () => {
|
// Load persisted accounts from localStorage
|
||||||
try {
|
try {
|
||||||
const json = JSON.parse(localStorage.getItem('accounts') || '[]')
|
const json = JSON.parse(localStorage.getItem('accounts') || '[]')
|
||||||
await accounts.fromJSON(json)
|
await accounts.fromJSON(json)
|
||||||
@@ -46,65 +77,72 @@ function App() {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to load accounts from storage:', err)
|
console.error('Failed to load accounts from storage:', err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Subscribe to accounts changes and persist to localStorage
|
||||||
|
const accountsSub = accounts.accounts$.subscribe(() => {
|
||||||
|
localStorage.setItem('accounts', JSON.stringify(accounts.toJSON()))
|
||||||
|
})
|
||||||
|
|
||||||
|
// Subscribe to active account changes and persist to localStorage
|
||||||
|
const activeSub = accounts.active$.subscribe((account) => {
|
||||||
|
if (account) {
|
||||||
|
localStorage.setItem('active', account.id)
|
||||||
|
} else {
|
||||||
|
localStorage.removeItem('active')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const pool = new RelayPool()
|
||||||
|
|
||||||
|
// Define relay URLs for bookmark fetching
|
||||||
|
const relayUrls = [
|
||||||
|
'wss://relay.damus.io',
|
||||||
|
'wss://nos.lol',
|
||||||
|
'wss://relay.nostr.band',
|
||||||
|
'wss://relay.dergigi.com',
|
||||||
|
'wss://wot.dergigi.com',
|
||||||
|
'wss://relay.snort.social',
|
||||||
|
'wss://relay.current.fyi',
|
||||||
|
'wss://nostr-pub.wellorder.net'
|
||||||
|
]
|
||||||
|
|
||||||
|
// Create a relay group for better event deduplication and management
|
||||||
|
// This follows the applesauce-relay documentation pattern
|
||||||
|
// Note: We could use pool.group(relayUrls) for direct requests in the future
|
||||||
|
pool.group(relayUrls)
|
||||||
|
console.log('Created relay group with', relayUrls.length, 'relays')
|
||||||
|
console.log('Relay URLs:', relayUrls)
|
||||||
|
|
||||||
|
// Attach address/replaceable loaders so ProfileModel can fetch profiles
|
||||||
|
const addressLoader = createAddressLoader(pool, {
|
||||||
|
eventStore: store,
|
||||||
|
lookupRelays: [
|
||||||
|
'wss://purplepag.es',
|
||||||
|
'wss://relay.primal.net',
|
||||||
|
'wss://relay.nostr.band'
|
||||||
|
]
|
||||||
|
})
|
||||||
|
store.addressableLoader = addressLoader
|
||||||
|
store.replaceableLoader = addressLoader
|
||||||
|
|
||||||
|
setEventStore(store)
|
||||||
|
setAccountManager(accounts)
|
||||||
|
setRelayPool(pool)
|
||||||
|
|
||||||
|
// Cleanup function
|
||||||
|
return () => {
|
||||||
|
accountsSub.unsubscribe()
|
||||||
|
activeSub.unsubscribe()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
loadAccounts()
|
let cleanup: (() => void) | undefined
|
||||||
|
initializeApp().then((fn) => {
|
||||||
// Subscribe to accounts changes and persist to localStorage
|
cleanup = fn
|
||||||
const accountsSub = accounts.accounts$.subscribe(() => {
|
|
||||||
localStorage.setItem('accounts', JSON.stringify(accounts.toJSON()))
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// Subscribe to active account changes and persist to localStorage
|
|
||||||
const activeSub = accounts.active$.subscribe((account) => {
|
|
||||||
if (account) {
|
|
||||||
localStorage.setItem('active', account.id)
|
|
||||||
} else {
|
|
||||||
localStorage.removeItem('active')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const pool = new RelayPool()
|
|
||||||
|
|
||||||
// Define relay URLs for bookmark fetching
|
|
||||||
const relayUrls = [
|
|
||||||
'wss://relay.damus.io',
|
|
||||||
'wss://nos.lol',
|
|
||||||
'wss://relay.nostr.band',
|
|
||||||
'wss://relay.dergigi.com',
|
|
||||||
'wss://wot.dergigi.com',
|
|
||||||
'wss://relay.snort.social',
|
|
||||||
'wss://relay.current.fyi',
|
|
||||||
'wss://nostr-pub.wellorder.net'
|
|
||||||
]
|
|
||||||
|
|
||||||
// Create a relay group for better event deduplication and management
|
|
||||||
// This follows the applesauce-relay documentation pattern
|
|
||||||
// Note: We could use pool.group(relayUrls) for direct requests in the future
|
|
||||||
pool.group(relayUrls)
|
|
||||||
console.log('Created relay group with', relayUrls.length, 'relays')
|
|
||||||
console.log('Relay URLs:', relayUrls)
|
|
||||||
|
|
||||||
// Attach address/replaceable loaders so ProfileModel can fetch profiles
|
|
||||||
const addressLoader = createAddressLoader(pool, {
|
|
||||||
eventStore: store,
|
|
||||||
lookupRelays: [
|
|
||||||
'wss://purplepag.es',
|
|
||||||
'wss://relay.primal.net',
|
|
||||||
'wss://relay.nostr.band'
|
|
||||||
]
|
|
||||||
})
|
|
||||||
store.addressableLoader = addressLoader
|
|
||||||
store.replaceableLoader = addressLoader
|
|
||||||
|
|
||||||
setEventStore(store)
|
|
||||||
setAccountManager(accounts)
|
|
||||||
setRelayPool(pool)
|
|
||||||
|
|
||||||
// Cleanup subscriptions on unmount
|
|
||||||
return () => {
|
return () => {
|
||||||
accountsSub.unsubscribe()
|
if (cleanup) cleanup()
|
||||||
activeSub.unsubscribe()
|
|
||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
@@ -121,26 +159,7 @@ function App() {
|
|||||||
<AccountsProvider manager={accountManager}>
|
<AccountsProvider manager={accountManager}>
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<div className="app">
|
<div className="app">
|
||||||
<Routes>
|
<AppRoutes relayPool={relayPool} showToast={showToast} />
|
||||||
<Route
|
|
||||||
path="/a/:naddr"
|
|
||||||
element={
|
|
||||||
<Bookmarks
|
|
||||||
relayPool={relayPool}
|
|
||||||
onLogout={() => {
|
|
||||||
if (accountManager) {
|
|
||||||
accountManager.setActive(undefined as never)
|
|
||||||
localStorage.removeItem('active')
|
|
||||||
showToast('Logged out successfully')
|
|
||||||
console.log('Logged out')
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Route path="/" element={<Navigate to={`/a/${DEFAULT_ARTICLE}`} replace />} />
|
|
||||||
<Route path="/login" element={<Login onLogin={() => showToast('Logged in successfully')} />} />
|
|
||||||
</Routes>
|
|
||||||
</div>
|
</div>
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
{toastMessage && (
|
{toastMessage && (
|
||||||
|
|||||||
@@ -1,47 +0,0 @@
|
|||||||
import React, { useState } from 'react'
|
|
||||||
import { Hooks } from 'applesauce-react'
|
|
||||||
import { Accounts } from 'applesauce-accounts'
|
|
||||||
|
|
||||||
interface LoginProps {
|
|
||||||
onLogin: () => void
|
|
||||||
}
|
|
||||||
|
|
||||||
const Login: React.FC<LoginProps> = ({ onLogin }) => {
|
|
||||||
const [isConnecting, setIsConnecting] = useState(false)
|
|
||||||
const accountManager = Hooks.useAccountManager()
|
|
||||||
|
|
||||||
const handleLogin = async () => {
|
|
||||||
try {
|
|
||||||
setIsConnecting(true)
|
|
||||||
|
|
||||||
// Create account from nostr extension
|
|
||||||
const account = await Accounts.ExtensionAccount.fromExtension()
|
|
||||||
accountManager.addAccount(account)
|
|
||||||
accountManager.setActive(account)
|
|
||||||
onLogin()
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Login failed:', error)
|
|
||||||
alert('Login failed. Please install a nostr browser extension and try again.')
|
|
||||||
} finally {
|
|
||||||
setIsConnecting(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="login-container">
|
|
||||||
<div className="login-card">
|
|
||||||
<h2>Welcome to Boris</h2>
|
|
||||||
<p>Connect your nostr account to view your bookmarks</p>
|
|
||||||
<button
|
|
||||||
onClick={handleLogin}
|
|
||||||
disabled={isConnecting}
|
|
||||||
className="login-button"
|
|
||||||
>
|
|
||||||
{isConnecting ? 'Connecting...' : 'Connect with Nostr'}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Login
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { useState, useEffect, useRef } from 'react'
|
import React, { useState, useEffect, useRef } from 'react'
|
||||||
import { faTimes, faList, faThLarge, faImage, faUnderline, faHighlighter } from '@fortawesome/free-solid-svg-icons'
|
import { faTimes, faList, faThLarge, faImage, faUnderline, faHighlighter, faUndo } from '@fortawesome/free-solid-svg-icons'
|
||||||
import { UserSettings } from '../services/settingsService'
|
import { UserSettings } from '../services/settingsService'
|
||||||
import IconButton from './IconButton'
|
import IconButton from './IconButton'
|
||||||
import ColorPicker from './ColorPicker'
|
import ColorPicker from './ColorPicker'
|
||||||
@@ -7,6 +7,21 @@ import FontSelector from './FontSelector'
|
|||||||
import { loadFont, getFontFamily } from '../utils/fontLoader'
|
import { loadFont, getFontFamily } from '../utils/fontLoader'
|
||||||
import { hexToRgb } from '../utils/colorHelpers'
|
import { hexToRgb } from '../utils/colorHelpers'
|
||||||
|
|
||||||
|
const DEFAULT_SETTINGS: UserSettings = {
|
||||||
|
collapseOnArticleOpen: true,
|
||||||
|
defaultViewMode: 'compact',
|
||||||
|
showHighlights: true,
|
||||||
|
sidebarCollapsed: true,
|
||||||
|
highlightsCollapsed: true,
|
||||||
|
readingFont: 'source-serif-4',
|
||||||
|
fontSize: 18,
|
||||||
|
highlightStyle: 'marker',
|
||||||
|
highlightColor: '#ffff00',
|
||||||
|
highlightColorNostrverse: '#9333ea',
|
||||||
|
highlightColorFriends: '#f97316',
|
||||||
|
highlightColorMine: '#ffff00',
|
||||||
|
}
|
||||||
|
|
||||||
interface SettingsProps {
|
interface SettingsProps {
|
||||||
settings: UserSettings
|
settings: UserSettings
|
||||||
onSave: (settings: UserSettings) => Promise<void>
|
onSave: (settings: UserSettings) => Promise<void>
|
||||||
@@ -45,17 +60,32 @@ const Settings: React.FC<SettingsProps> = ({ settings, onSave, onClose }) => {
|
|||||||
|
|
||||||
const previewFontFamily = getFontFamily(localSettings.readingFont || 'source-serif-4')
|
const previewFontFamily = getFontFamily(localSettings.readingFont || 'source-serif-4')
|
||||||
|
|
||||||
|
const handleResetToDefaults = () => {
|
||||||
|
if (confirm('Reset all settings to defaults?')) {
|
||||||
|
setLocalSettings(DEFAULT_SETTINGS)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="settings-view">
|
<div className="settings-view">
|
||||||
<div className="settings-header">
|
<div className="settings-header">
|
||||||
<h2>Settings</h2>
|
<h2>Settings</h2>
|
||||||
<IconButton
|
<div className="settings-header-actions">
|
||||||
icon={faTimes}
|
<IconButton
|
||||||
onClick={onClose}
|
icon={faUndo}
|
||||||
title="Close settings"
|
onClick={handleResetToDefaults}
|
||||||
ariaLabel="Close settings"
|
title="Reset to defaults"
|
||||||
variant="ghost"
|
ariaLabel="Reset to defaults"
|
||||||
/>
|
variant="ghost"
|
||||||
|
/>
|
||||||
|
<IconButton
|
||||||
|
icon={faTimes}
|
||||||
|
onClick={onClose}
|
||||||
|
title="Close settings"
|
||||||
|
ariaLabel="Close settings"
|
||||||
|
variant="ghost"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="settings-content">
|
<div className="settings-content">
|
||||||
|
|||||||
@@ -56,53 +56,6 @@ body {
|
|||||||
color: #888;
|
color: #888;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Login Styles */
|
|
||||||
.login-container {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
min-height: 50vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-card {
|
|
||||||
background: #1a1a1a;
|
|
||||||
padding: 2rem;
|
|
||||||
border-radius: 8px;
|
|
||||||
border: 1px solid #333;
|
|
||||||
max-width: 400px;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-card h2 {
|
|
||||||
margin: 0 0 1rem 0;
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-card p {
|
|
||||||
margin: 0 0 1.5rem 0;
|
|
||||||
color: #ccc;
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-button {
|
|
||||||
background: #646cff;
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
padding: 0.75rem 1.5rem;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-size: 1rem;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: background-color 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-button:hover:not(:disabled) {
|
|
||||||
background: #535bf2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-button:disabled {
|
|
||||||
opacity: 0.6;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Bookmarks Styles */
|
/* Bookmarks Styles */
|
||||||
.bookmarks-container {
|
.bookmarks-container {
|
||||||
background: #1a1a1a;
|
background: #1a1a1a;
|
||||||
@@ -1147,7 +1100,6 @@ body {
|
|||||||
background-color: #ffffff;
|
background-color: #ffffff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-card,
|
|
||||||
.bookmark-item {
|
.bookmark-item {
|
||||||
background: #f9f9f9;
|
background: #f9f9f9;
|
||||||
border-color: #ddd;
|
border-color: #ddd;
|
||||||
@@ -1846,6 +1798,12 @@ body {
|
|||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.settings-header-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
.settings-content {
|
.settings-content {
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
|||||||
@@ -8,9 +8,9 @@ export function hexToRgb(hex: string): string {
|
|||||||
|
|
||||||
export const HIGHLIGHT_COLORS = [
|
export const HIGHLIGHT_COLORS = [
|
||||||
{ name: 'Yellow', value: '#ffff00' },
|
{ name: 'Yellow', value: '#ffff00' },
|
||||||
{ name: 'Orange', value: '#ff9500' },
|
{ name: 'Orange', value: '#f97316' },
|
||||||
{ name: 'Pink', value: '#ff69b4' },
|
{ name: 'Pink', value: '#ff69b4' },
|
||||||
{ name: 'Green', value: '#00ff7f' },
|
{ name: 'Green', value: '#00ff7f' },
|
||||||
{ name: 'Blue', value: '#4da6ff' },
|
{ name: 'Blue', value: '#4da6ff' },
|
||||||
{ name: 'Purple', value: '#b19cd9' }
|
{ name: 'Purple', value: '#9333ea' }
|
||||||
]
|
]
|
||||||
|
|||||||
Reference in New Issue
Block a user