mirror of
https://github.com/uqbar-dao/nectar.git
synced 2025-01-09 03:00:48 +03:00
additional ui feedback
This commit is contained in:
parent
df3c0bdbb3
commit
542a3362d4
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -14,8 +14,8 @@
|
||||
<meta httpEquiv="X-UA-Compatible" content="IE=edge" />
|
||||
<meta name="viewport"
|
||||
content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1.00001, viewport-fit=cover" />
|
||||
<script type="module" crossorigin src="/main:app_store:sys/assets/index-C8ofW7WS.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/main:app_store:sys/assets/index-_Aqzph9X.css">
|
||||
<script type="module" crossorigin src="/main:app_store:sys/assets/index-nztUlpw-.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/main:app_store:sys/assets/index-zq6Um8Qt.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
@ -4,6 +4,7 @@ import { appId } from "../utils/app";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import classNames from "classnames";
|
||||
import { APP_DETAILS_PATH } from "../constants/path";
|
||||
import ColorDot from "./ColorDot";
|
||||
|
||||
interface AppHeaderProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
app: AppInfo;
|
||||
@ -23,11 +24,17 @@ export default function AppHeader({
|
||||
className={classNames('flex w-full justify-content-start', size, props.className, { 'cursor-pointer': size !== 'large' })}
|
||||
onClick={() => navigate(`/${APP_DETAILS_PATH}/${appId(app)}`)}
|
||||
>
|
||||
{app.metadata?.image && <img
|
||||
src={app.metadata.image}
|
||||
alt="app icon"
|
||||
className={classNames('mr-2', { 'h-32 rounded-md': size === 'large', 'h-12 rounded': size !== 'large' })}
|
||||
/>}
|
||||
{app.metadata?.image
|
||||
? <img
|
||||
src={app.metadata.image}
|
||||
alt="app icon"
|
||||
className={classNames('mr-2', { 'h-32 rounded-md': size === 'large', 'h-12 rounded': size !== 'large' })}
|
||||
/>
|
||||
: <ColorDot
|
||||
num={app.metadata_hash}
|
||||
dotSize={size}
|
||||
className={classNames('mr-2')}
|
||||
/>}
|
||||
<div className="flex flex-col w-full">
|
||||
<div
|
||||
className={classNames("whitespace-nowrap overflow-hidden text-ellipsis", { 'text-3xl': size === 'large', })}
|
||||
|
56
kinode/packages/app_store/ui/src/components/ColorDot.tsx
Normal file
56
kinode/packages/app_store/ui/src/components/ColorDot.tsx
Normal file
@ -0,0 +1,56 @@
|
||||
import classNames from 'classnames'
|
||||
import React from 'react'
|
||||
import { hexToRgb, hslToRgb, rgbToHex, rgbToHsl } from '../utils/colors'
|
||||
import { isMobileCheck } from '../utils/dimensions'
|
||||
|
||||
interface ColorDotProps extends React.HTMLAttributes<HTMLSpanElement> {
|
||||
num: string,
|
||||
dotSize?: 'small' | 'medium' | 'large'
|
||||
}
|
||||
|
||||
const ColorDot: React.FC<ColorDotProps> = ({
|
||||
num,
|
||||
dotSize,
|
||||
...props
|
||||
}) => {
|
||||
const isMobile = isMobileCheck()
|
||||
|
||||
num = (num || '').replace(/(0x|\.)/g, '')
|
||||
|
||||
while (num.length < 6) {
|
||||
num = '0' + num
|
||||
}
|
||||
|
||||
const leftHsl = rgbToHsl(hexToRgb(num.slice(0, 6)))
|
||||
const rightHsl = rgbToHsl(hexToRgb(num.length > 6 ? num.slice(num.length - 6) : num))
|
||||
leftHsl.s = rightHsl.s = 1
|
||||
const leftColor = rgbToHex(hslToRgb(leftHsl))
|
||||
const rightColor = rgbToHex(hslToRgb(rightHsl))
|
||||
|
||||
const angle = (parseInt(num, 16) % 360) || -45
|
||||
|
||||
return (
|
||||
<div {...props} className={classNames('flex', props.className)}>
|
||||
<div
|
||||
className={classNames('m-0 align-self-center border rounded-full outline-black', {
|
||||
'h-20 w-20': !isMobile && dotSize === 'large',
|
||||
'h-16 w-16': !isMobile && dotSize === 'medium',
|
||||
'h-12 w-12': isMobile || dotSize === 'small',
|
||||
'border-4': !isMobile,
|
||||
'border-2': isMobile,
|
||||
})}
|
||||
style={{
|
||||
borderTopColor: leftColor,
|
||||
borderRightColor: rightColor,
|
||||
borderBottomColor: rightColor,
|
||||
borderLeftColor: leftColor,
|
||||
background: `linear-gradient(${angle}deg, ${leftColor} 0 50%, ${rightColor} 50% 100%)`,
|
||||
filter: 'saturate(0.25)',
|
||||
opacity: '0.75'
|
||||
}} />
|
||||
{props.children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ColorDot
|
106
kinode/packages/app_store/ui/src/utils/colors.ts
Normal file
106
kinode/packages/app_store/ui/src/utils/colors.ts
Normal file
@ -0,0 +1,106 @@
|
||||
export type RgbType = { r: number, g: number, b: number }
|
||||
export type HslType = { h: number, s: number, l: number }
|
||||
|
||||
export const rgbToHsl: (rgb: RgbType) => HslType = ({ r, g, b }) => {
|
||||
r /= 255; g /= 255; b /= 255;
|
||||
let max = Math.max(r, g, b);
|
||||
let min = Math.min(r, g, b);
|
||||
let d = max - min;
|
||||
let h = 0;
|
||||
if (d === 0) h = 0;
|
||||
else if (max === r) h = ((((g - b) / d) % 6) + 6) % 6; // the javascript modulo operator handles negative numbers differently than most other languages
|
||||
else if (max === g) h = (b - r) / d + 2;
|
||||
else if (max === b) h = (r - g) / d + 4;
|
||||
let l = (min + max) / 2;
|
||||
let s = d === 0 ? 0 : d / (1 - Math.abs(2 * l - 1));
|
||||
return { h: h * 60, s, l };
|
||||
}
|
||||
|
||||
export const hslToRgb: (hsl: HslType) => RgbType = ({ h, s, l }) => {
|
||||
let c = (1 - Math.abs(2 * l - 1)) * s;
|
||||
let hp = h / 60.0;
|
||||
let x = c * (1 - Math.abs((hp % 2) - 1));
|
||||
let rgb1 = [0, 0, 0];
|
||||
if (isNaN(h)) rgb1 = [0, 0, 0];
|
||||
else if (hp <= 1) rgb1 = [c, x, 0];
|
||||
else if (hp <= 2) rgb1 = [x, c, 0];
|
||||
else if (hp <= 3) rgb1 = [0, c, x];
|
||||
else if (hp <= 4) rgb1 = [0, x, c];
|
||||
else if (hp <= 5) rgb1 = [x, 0, c];
|
||||
else if (hp <= 6) rgb1 = [c, 0, x];
|
||||
let m = l - c * 0.5;
|
||||
return {
|
||||
r: Math.round(255 * (rgb1[0] + m)),
|
||||
g: Math.round(255 * (rgb1[1] + m)),
|
||||
b: Math.round(255 * (rgb1[2] + m))
|
||||
};
|
||||
}
|
||||
|
||||
export const hexToRgb: (hex: string) => RgbType = (hex) => {
|
||||
var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
||||
return result ? {
|
||||
r: parseInt(result[1], 16),
|
||||
g: parseInt(result[2], 16),
|
||||
b: parseInt(result[3], 16)
|
||||
} : { r: 0, g: 0, b: 0 };
|
||||
}
|
||||
|
||||
export const rgbToHex: (rgb: RgbType) => string = ({ r, g, b }) => {
|
||||
return '#' + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1);
|
||||
}
|
||||
|
||||
export const generateDistinguishableColors = (numColors: number, mod?: number, sat?: number, val?: number) => {
|
||||
const colors: string[] = [];
|
||||
const saturation = sat || 0.75;
|
||||
const value = val || 0.75;
|
||||
|
||||
for (let i = 0; i < numColors; i++) {
|
||||
const hue = i / numColors * (+(mod || 1) || 1);
|
||||
const [r, g, b] = hsvToRgb(hue, saturation, value);
|
||||
const hexColor = rgbToHex2(r, g, b);
|
||||
colors.push(hexColor);
|
||||
}
|
||||
|
||||
return colors;
|
||||
}
|
||||
|
||||
export const hsvToRgb = (h: number, s: number, v: number) => {
|
||||
let [r, g, b] = [0, 0, 0];
|
||||
const i = Math.floor(h * 6);
|
||||
const f = h * 6 - i;
|
||||
const p = v * (1 - s);
|
||||
const q = v * (1 - f * s);
|
||||
const t = v * (1 - (1 - f) * s);
|
||||
|
||||
switch (i % 6) {
|
||||
case 0:
|
||||
(r = v), (g = t), (b = p); // eslint-disable-line
|
||||
break;
|
||||
case 1:
|
||||
(r = q), (g = v), (b = p); // eslint-disable-line
|
||||
break;
|
||||
case 2:
|
||||
(r = p), (g = v), (b = t); // eslint-disable-line
|
||||
break;
|
||||
case 3:
|
||||
(r = p), (g = q), (b = v); // eslint-disable-line
|
||||
break;
|
||||
case 4:
|
||||
(r = t), (g = p), (b = v); // eslint-disable-line
|
||||
break;
|
||||
case 5:
|
||||
(r = v), (g = p), (b = q); // eslint-disable-line
|
||||
break;
|
||||
}
|
||||
|
||||
return [Math.round(r * 255), Math.round(g * 255), Math.round(b * 255)];
|
||||
}
|
||||
|
||||
export const componentToHex = (c: number) => {
|
||||
const hex = c.toString(16);
|
||||
return hex.length === 1 ? '0' + hex : hex;
|
||||
}
|
||||
|
||||
export const rgbToHex2 = (r: number, g: number, b: number) => {
|
||||
return '#' + componentToHex(r) + componentToHex(g) + componentToHex(b);
|
||||
}
|
1
kinode/packages/app_store/ui/src/utils/dimensions.ts
Normal file
1
kinode/packages/app_store/ui/src/utils/dimensions.ts
Normal file
@ -0,0 +1 @@
|
||||
export const isMobileCheck = () => window.innerWidth <= 600
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -9,8 +9,8 @@
|
||||
<meta httpEquiv="X-UA-Compatible" content="IE=edge" />
|
||||
<meta name="viewport"
|
||||
content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1.00001, viewport-fit=cover" />
|
||||
<script type="module" crossorigin src="/assets/index-BMzDi7wQ.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-1PguYU6W.css">
|
||||
<script type="module" crossorigin src="/assets/index-CiPfZ2kc.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-C9o9YkgK.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
4
kinode/packages/homepage/ui/dist/index.html
vendored
4
kinode/packages/homepage/ui/dist/index.html
vendored
@ -9,8 +9,8 @@
|
||||
<meta httpEquiv="X-UA-Compatible" content="IE=edge" />
|
||||
<meta name="viewport"
|
||||
content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1.00001, viewport-fit=cover" />
|
||||
<script type="module" crossorigin src="/assets/index-BMzDi7wQ.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-1PguYU6W.css">
|
||||
<script type="module" crossorigin src="/assets/index-CiPfZ2kc.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-C9o9YkgK.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
@ -1,29 +1,20 @@
|
||||
import classNames from "classnames"
|
||||
import useHomepageStore, { HomepageApp } from "../store/homepageStore"
|
||||
import useHomepageStore from "../store/homepageStore"
|
||||
import { isMobileCheck } from "../utilities/dimensions"
|
||||
import AppDisplay from "./AppDisplay"
|
||||
import usePersistentStore from "../store/persistentStore"
|
||||
import { useEffect, useState } from "react"
|
||||
|
||||
const AllApps: React.FC = () => {
|
||||
const AllApps: React.FC<{ expanded: boolean }> = ({ expanded }) => {
|
||||
const { apps } = useHomepageStore()
|
||||
const { favoriteApps } = usePersistentStore()
|
||||
const isMobile = isMobileCheck()
|
||||
const [undockedApps, setUndockedApps] = useState<HomepageApp[]>([])
|
||||
|
||||
useEffect(() => {
|
||||
setUndockedApps(apps.filter(a => !favoriteApps[a.package_name]))
|
||||
}, [apps, favoriteApps])
|
||||
|
||||
return <div className={classNames('flex-center flex-wrap overflow-y-auto flex-grow self-stretch', {
|
||||
'px-8 gap-4': isMobile,
|
||||
'px-16 gap-8': !isMobile
|
||||
return <div className={classNames('flex-center flex-wrap gap-4 overflow-y-auto self-stretch relative', {
|
||||
'max-h-0': !expanded,
|
||||
'p-8 max-h-[1000px]': expanded,
|
||||
'placeholder': isMobile
|
||||
})}>
|
||||
{undockedApps.length === 0
|
||||
? (apps && apps.length === 0)
|
||||
? <div>Loading apps...</div>
|
||||
: <></>
|
||||
: undockedApps.map(app => <AppDisplay app={app} />)}
|
||||
{apps.length === 0
|
||||
? <div>Loading apps...</div>
|
||||
: apps.map(app => <AppDisplay app={app} />)}
|
||||
</div>
|
||||
}
|
||||
|
||||
|
@ -16,7 +16,7 @@ const AppDisplay: React.FC<AppDisplayProps> = ({ app }) => {
|
||||
const isMobile = isMobileCheck()
|
||||
|
||||
return <a
|
||||
className={classNames("flex-col-center gap-2 relative hover:opacity-90", {
|
||||
className={classNames("flex-col-center gap-2 relative hover:opacity-90 transition-opacity", {
|
||||
'cursor-pointer': app.path,
|
||||
'pointer-events-none': !app.path,
|
||||
})}
|
||||
@ -29,8 +29,8 @@ const AppDisplay: React.FC<AppDisplayProps> = ({ app }) => {
|
||||
? <img
|
||||
src={app.base64_icon}
|
||||
className={classNames('rounded', {
|
||||
'h-12 w-12': isMobile,
|
||||
'h-32 w-32': !isMobile
|
||||
'h-8 w-8': isMobile,
|
||||
'h-16 w-16': !isMobile
|
||||
})}
|
||||
/>
|
||||
: <ColorDot num={app.state?.our_version || '0'} />}
|
||||
|
@ -31,8 +31,8 @@ const ColorDot: React.FC<ColorDotProps> = ({
|
||||
<div {...props} className={classNames('flex', props.className)}>
|
||||
<div
|
||||
className={classNames('m-0 align-self-center border rounded-full outline-black', {
|
||||
'h-32 w-32 border-8': !isMobile,
|
||||
'h-12 w-12 border-3': isMobile
|
||||
'h-16 w-16 border-4': !isMobile,
|
||||
'h-8 w-8 border-2': isMobile
|
||||
})}
|
||||
style={{
|
||||
borderTopColor: leftColor,
|
||||
@ -40,7 +40,8 @@ const ColorDot: React.FC<ColorDotProps> = ({
|
||||
borderBottomColor: rightColor,
|
||||
borderLeftColor: leftColor,
|
||||
background: `linear-gradient(${angle}deg, ${leftColor} 0 50%, ${rightColor} 50% 100%)`,
|
||||
filter: 'saturate(0.25)'
|
||||
filter: 'saturate(0.25)',
|
||||
opacity: '0.75'
|
||||
}} />
|
||||
{props.children}
|
||||
</div>
|
||||
|
@ -36,7 +36,7 @@ const Widget: React.FC<WidgetProps> = ({ package_name, widget, forceLarge }) =>
|
||||
data-widget-code={widget}
|
||||
/>
|
||||
{isHovered && <button
|
||||
className="absolute top-0 left-2 icon"
|
||||
className="absolute top-0 left-0 icon"
|
||||
onClick={() => toggleWidgetVisibility(package_name)}
|
||||
>
|
||||
{widgetSettings[package_name]?.hide ? <FaEye /> : <FaEyeSlash />}
|
||||
|
@ -1,39 +1,25 @@
|
||||
import { FaGear } from "react-icons/fa6"
|
||||
import useHomepageStore from "../store/homepageStore"
|
||||
import Widget from "./Widget"
|
||||
import WidgetsSettingsModal from "./WidgetsSettingsModal"
|
||||
import usePersistentStore from "../store/persistentStore"
|
||||
import { isMobileCheck } from "../utilities/dimensions"
|
||||
import classNames from "classnames"
|
||||
|
||||
const Widgets = () => {
|
||||
const { apps, showWidgetsSettings, setShowWidgetsSettings } = useHomepageStore()
|
||||
const { apps } = useHomepageStore()
|
||||
const { widgetSettings } = usePersistentStore();
|
||||
const isMobile = isMobileCheck()
|
||||
|
||||
return <div className={classNames("flex-col-center flex-grow self-stretch relative", {
|
||||
'm-4': !isMobile,
|
||||
'm-2': isMobile
|
||||
return <div className={classNames("flex-center flex-wrap flex-grow self-stretch", {
|
||||
'gap-2 m-2': isMobile,
|
||||
'gap-4 m-4': !isMobile
|
||||
})}>
|
||||
<button
|
||||
className="icon ml-4 absolute top-0 right-4"
|
||||
onClick={() => setShowWidgetsSettings(true)}
|
||||
>
|
||||
<FaGear />
|
||||
</button>
|
||||
<div className={classNames("flex-center flex-wrap flex-grow self-stretch", {
|
||||
'gap-2': isMobile,
|
||||
'gap-4': !isMobile
|
||||
})}>
|
||||
{apps
|
||||
.filter(app => app.widget)
|
||||
.map(({ widget, package_name }, _i, _appsWithWidgets) => !widgetSettings[package_name]?.hide && <Widget
|
||||
package_name={package_name}
|
||||
widget={widget!}
|
||||
forceLarge={_appsWithWidgets.length === 1}
|
||||
/>)}
|
||||
</div>
|
||||
{showWidgetsSettings && <WidgetsSettingsModal />}
|
||||
{apps
|
||||
.filter(app => app.widget)
|
||||
.map(({ widget, package_name }, _i, _appsWithWidgets) => !widgetSettings[package_name]?.hide && <Widget
|
||||
package_name={package_name}
|
||||
widget={widget!}
|
||||
forceLarge={_appsWithWidgets.length === 1}
|
||||
/>)}
|
||||
</div>
|
||||
}
|
||||
|
||||
|
@ -2,12 +2,13 @@ import { useEffect, useState } from 'react'
|
||||
import KinodeText from '../components/KinodeText'
|
||||
import KinodeBird from '../components/KinodeBird'
|
||||
import useHomepageStore from '../store/homepageStore'
|
||||
import { FaGear, FaV } from 'react-icons/fa6'
|
||||
import { FaChevronDown, FaChevronUp, FaScrewdriverWrench, FaV } from 'react-icons/fa6'
|
||||
import AppsDock from '../components/AppsDock'
|
||||
import AllApps from '../components/AllApps'
|
||||
import Widgets from '../components/Widgets'
|
||||
import { isMobileCheck } from '../utilities/dimensions'
|
||||
import classNames from 'classnames'
|
||||
import WidgetsSettingsModal from '../components/WidgetsSettingsModal'
|
||||
|
||||
interface AppStoreApp {
|
||||
package: string,
|
||||
@ -18,7 +19,8 @@ interface AppStoreApp {
|
||||
}
|
||||
function Homepage() {
|
||||
const [our, setOur] = useState('')
|
||||
const { apps, setApps, isHosted, fetchHostedStatus } = useHomepageStore()
|
||||
const [allAppsExpanded, setAllAppsExpanded] = useState(false)
|
||||
const { apps, setApps, isHosted, fetchHostedStatus, showWidgetsSettings, setShowWidgetsSettings } = useHomepageStore()
|
||||
const isMobile = isMobileCheck()
|
||||
|
||||
useEffect(() => {
|
||||
@ -60,9 +62,7 @@ function Homepage() {
|
||||
}, [our])
|
||||
|
||||
return (
|
||||
<div className={classNames("flex-col-center relative w-screen overflow-hidden special-bg-homepage", {
|
||||
'h-screen': !isMobile,
|
||||
'min-h-screen': isMobile
|
||||
<div className={classNames("flex-col-center relative w-screen overflow-hidden special-bg-homepage min-h-screen", {
|
||||
})}>
|
||||
<h5 className={classNames('absolute flex gap-4 c', {
|
||||
'top-8 left-8 right-8': !isMobile,
|
||||
@ -75,26 +75,36 @@ function Homepage() {
|
||||
<FaV />
|
||||
</a>}
|
||||
{our}
|
||||
<a
|
||||
href='/settings:settings:sys'
|
||||
className='button icon ml-auto'
|
||||
<button
|
||||
className="icon ml-auto"
|
||||
onClick={() => setShowWidgetsSettings(true)}
|
||||
>
|
||||
<FaGear />
|
||||
</a>
|
||||
<FaScrewdriverWrench />
|
||||
</button>
|
||||
</h5>
|
||||
{isMobile
|
||||
? <div className='flex-center gap-4 p-8 mt-8 max-w-screen'>
|
||||
<KinodeBird />
|
||||
<KinodeText />
|
||||
</div>
|
||||
: <div className={classNames("flex-col-center mx-0 gap-6 mt-8 mb-16")}>
|
||||
: <div className={classNames("flex-col-center mx-0 gap-4 mt-8 mb-4")}>
|
||||
<h3 className='text-center'>Welcome to</h3>
|
||||
<KinodeText />
|
||||
<KinodeBird />
|
||||
</div>}
|
||||
<AppsDock />
|
||||
<AllApps />
|
||||
<Widgets />
|
||||
<button
|
||||
className={classNames("clear flex-center self-center", {
|
||||
'-mb-1': !allAppsExpanded,
|
||||
})}
|
||||
onClick={() => setAllAppsExpanded(!allAppsExpanded)}
|
||||
>
|
||||
{allAppsExpanded ? <FaChevronDown /> : <FaChevronUp />}
|
||||
<span className="ml-2">{allAppsExpanded ? 'Collapse' : 'All installed apps'}</span>
|
||||
</button>
|
||||
<AllApps expanded={allAppsExpanded} />
|
||||
{showWidgetsSettings && <WidgetsSettingsModal />}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user