Back to hue-only role colors
This commit is contained in:
parent
1581d909f0
commit
8f7016b60c
3 changed files with 43 additions and 87 deletions
|
|
@ -24,7 +24,7 @@
|
||||||
loading = true
|
loading = true
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const error = await createRole(url, randomId(), label, description, color, 0)
|
const error = await createRole(url, randomId(), label, description, color, 1)
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
pushToast({theme: "error", message: error})
|
pushToast({theme: "error", message: error})
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<script module lang="ts">
|
<script module lang="ts">
|
||||||
import type {SpaceRole, SpaceRoleColor} from "@app/members"
|
import type {SpaceRole} from "@app/members"
|
||||||
|
|
||||||
export type Values = Pick<SpaceRole, "label" | "description" | "color">
|
export type Values = Pick<SpaceRole, "label" | "description" | "color">
|
||||||
</script>
|
</script>
|
||||||
|
|
@ -26,29 +26,11 @@
|
||||||
description: initialValues.description ?? "",
|
description: initialValues.description ?? "",
|
||||||
})
|
})
|
||||||
|
|
||||||
// Preserve any unedited components from the existing color; the form edits hue and lightness.
|
let hue = $state(initialValues.color ?? Math.floor(Math.random() * 360))
|
||||||
const baseColor: SpaceRoleColor = {
|
|
||||||
hue: "",
|
|
||||||
saturation: "",
|
|
||||||
lightness: "",
|
|
||||||
...initialValues.color,
|
|
||||||
}
|
|
||||||
|
|
||||||
const initialHue = parseInt(baseColor.hue, 10)
|
|
||||||
const initialLightness = parseFloat(baseColor.lightness)
|
|
||||||
|
|
||||||
let hue = $state(isNaN(initialHue) ? Math.floor(Math.random() * 360) : initialHue)
|
|
||||||
let lightness = $state(isNaN(initialLightness) ? 0.5 : initialLightness)
|
|
||||||
|
|
||||||
const color = $derived<SpaceRoleColor>({
|
|
||||||
...baseColor,
|
|
||||||
hue: String(hue),
|
|
||||||
lightness: String(lightness),
|
|
||||||
})
|
|
||||||
|
|
||||||
const back = () => history.back()
|
const back = () => history.back()
|
||||||
|
|
||||||
const submit = () => onSubmit({...values, color})
|
const submit = () => onSubmit({...values, color: hue})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex flex-col gap-4">
|
<div class="flex flex-col gap-4">
|
||||||
|
|
@ -76,25 +58,15 @@
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<div
|
<div
|
||||||
class="h-8 w-8 shrink-0 rounded-full border-2"
|
class="h-8 w-8 shrink-0 rounded-full border-2"
|
||||||
style="background-color: {roleColor(color)}; border-color: var(--line)">
|
style="background-color: {roleColor(hue)}; border-color: var(--line)">
|
||||||
</div>
|
|
||||||
<div class="flex grow flex-col gap-2">
|
|
||||||
<input
|
|
||||||
type="range"
|
|
||||||
min="0"
|
|
||||||
max="360"
|
|
||||||
bind:value={hue}
|
|
||||||
class="range"
|
|
||||||
style="color: {roleColor(color)}; --range-shdw: {roleColor(color)}" />
|
|
||||||
<input
|
|
||||||
type="range"
|
|
||||||
min="0"
|
|
||||||
max="1"
|
|
||||||
step="0.01"
|
|
||||||
bind:value={lightness}
|
|
||||||
class="range"
|
|
||||||
style="color: {roleColor(color)}; --range-shdw: {roleColor(color)}" />
|
|
||||||
</div>
|
</div>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min="0"
|
||||||
|
max="360"
|
||||||
|
bind:value={hue}
|
||||||
|
class="range grow"
|
||||||
|
style="color: {roleColor(hue)}; --range-shdw: {roleColor(hue)}" />
|
||||||
</div>
|
</div>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
</Field>
|
</Field>
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ import {
|
||||||
import type {Filter, PublishedRoomMeta, TrustedEvent} from "@welshman/util"
|
import type {Filter, PublishedRoomMeta, TrustedEvent} from "@welshman/util"
|
||||||
import {first, memoize, sortBy, spec, uniq} from "@welshman/lib"
|
import {first, memoize, sortBy, spec, uniq} from "@welshman/lib"
|
||||||
import {addRoomMember, manageRelay, pubkey, waitForThunkError} from "@welshman/app"
|
import {addRoomMember, manageRelay, pubkey, waitForThunkError} from "@welshman/app"
|
||||||
|
import {load} from "@welshman/net"
|
||||||
import {get} from "svelte/store"
|
import {get} from "svelte/store"
|
||||||
import {deriveEventsForUrl, deriveRelaySignedEvents} from "@app/repository"
|
import {deriveEventsForUrl, deriveRelaySignedEvents} from "@app/repository"
|
||||||
|
|
||||||
|
|
@ -32,60 +33,32 @@ export const deriveSpaceMembers = (url: string) =>
|
||||||
|
|
||||||
export const RELAY_ROLE = 33534
|
export const RELAY_ROLE = 33534
|
||||||
|
|
||||||
// An hsl color tuple. Any component may be an empty string, in which case the
|
|
||||||
// client supplies its own default. Usually only `hue` is set per the spec.
|
|
||||||
export type SpaceRoleColor = {
|
|
||||||
hue: string // 0 to 360
|
|
||||||
saturation: string // 0 to 1
|
|
||||||
lightness: string // 0 to 1
|
|
||||||
}
|
|
||||||
|
|
||||||
export type SpaceRole = {
|
export type SpaceRole = {
|
||||||
id: string
|
id: string
|
||||||
label: string
|
label: string
|
||||||
description: string
|
description: string
|
||||||
color: SpaceRoleColor
|
color: number // hue, 0 to 360
|
||||||
order: number
|
order: number
|
||||||
}
|
}
|
||||||
|
|
||||||
// Defaults filled in for empty color components, chosen to read on both themes.
|
// Defaults for the components the client supplies, chosen to read on both themes.
|
||||||
const DEFAULT_SATURATION = 0.7
|
const DEFAULT_SATURATION = 0.7
|
||||||
const DEFAULT_LIGHTNESS = 0.5
|
const DEFAULT_LIGHTNESS = 0.5
|
||||||
|
|
||||||
const roleColorValue = (value: string, fallback: number) => {
|
// Parse the hue from a ["color", hue] tag, falling back to 0.
|
||||||
const parsed = parseFloat(value)
|
export const parseRoleColor = (tags: string[][]): number => {
|
||||||
|
const hue = parseFloat(first(getTags("color", tags))?.[1] ?? "")
|
||||||
|
|
||||||
return isNaN(parsed) ? fallback : parsed
|
return isNaN(hue) ? 0 : hue
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse the ["color", hue, saturation, lightness] tag, preserving empty values.
|
// Build an hsl() string from a role's hue, using our default saturation and lightness.
|
||||||
export const parseRoleColor = (tags: string[][]): SpaceRoleColor => {
|
export const roleColor = (hue: number) =>
|
||||||
const tag = first(getTags("color", tags)) ?? []
|
`hsl(${hue}, ${DEFAULT_SATURATION * 100}%, ${DEFAULT_LIGHTNESS * 100}%)`
|
||||||
|
|
||||||
return {
|
|
||||||
hue: tag[1] ?? "",
|
|
||||||
saturation: tag[2] ?? "",
|
|
||||||
lightness: tag[3] ?? "",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build an hsl() string from a role color, using the role's values or our defaults.
|
|
||||||
export const roleColor = (color: SpaceRoleColor) => {
|
|
||||||
const h = roleColorValue(color.hue, 0)
|
|
||||||
const s = roleColorValue(color.saturation, DEFAULT_SATURATION)
|
|
||||||
const l = roleColorValue(color.lightness, DEFAULT_LIGHTNESS)
|
|
||||||
|
|
||||||
return `hsl(${h}, ${s * 100}%, ${l * 100}%)`
|
|
||||||
}
|
|
||||||
|
|
||||||
// A translucent tint of the role color for use as a background fill.
|
// A translucent tint of the role color for use as a background fill.
|
||||||
export const roleColorSoft = (color: SpaceRoleColor) => {
|
export const roleColorSoft = (hue: number) =>
|
||||||
const h = roleColorValue(color.hue, 0)
|
`hsl(${hue}, ${DEFAULT_SATURATION * 100}%, ${DEFAULT_LIGHTNESS * 100}%, 0.15)`
|
||||||
const s = roleColorValue(color.saturation, DEFAULT_SATURATION)
|
|
||||||
const l = roleColorValue(color.lightness, DEFAULT_LIGHTNESS)
|
|
||||||
|
|
||||||
return `hsl(${h}, ${s * 100}%, ${l * 100}%, 0.15)`
|
|
||||||
}
|
|
||||||
|
|
||||||
export const deriveSpaceRoles = (url: string) =>
|
export const deriveSpaceRoles = (url: string) =>
|
||||||
derived(deriveRelaySignedEvents(url, [{kinds: [RELAY_ROLE]}]), $events => {
|
derived(deriveRelaySignedEvents(url, [{kinds: [RELAY_ROLE]}]), $events => {
|
||||||
|
|
@ -100,7 +73,7 @@ export const deriveSpaceRoles = (url: string) =>
|
||||||
label: getTagValue("label", event.tags) ?? "",
|
label: getTagValue("label", event.tags) ?? "",
|
||||||
description: getTagValue("description", event.tags) ?? "",
|
description: getTagValue("description", event.tags) ?? "",
|
||||||
color: parseRoleColor(event.tags),
|
color: parseRoleColor(event.tags),
|
||||||
order: parseInt(getTagValue("order", event.tags) ?? "0", 10) || 0,
|
order: Math.max(1, parseInt(getTagValue("order", event.tags) ?? "1", 10) || 1),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -127,23 +100,29 @@ export const deriveSpaceMemberRoles = (url: string) =>
|
||||||
return memberRoles
|
return memberRoles
|
||||||
})
|
})
|
||||||
|
|
||||||
// Flatten a color tuple into management params, mirroring the color tag's value order.
|
// Pull the relay's freshly-signed role event so the UI reflects the change immediately,
|
||||||
const roleColorParams = (color: SpaceRoleColor) =>
|
// rather than waiting for the live subscription to catch up.
|
||||||
[color.hue, color.saturation, color.lightness] as unknown as string
|
const reloadRole = (url: string, id: string) =>
|
||||||
|
load({relays: [url], filters: [{kinds: [RELAY_ROLE], "#d": [id]}]})
|
||||||
|
|
||||||
export const createRole = async (
|
export const createRole = async (
|
||||||
url: string,
|
url: string,
|
||||||
id: string,
|
id: string,
|
||||||
label: string,
|
label: string,
|
||||||
description: string,
|
description: string,
|
||||||
color: SpaceRoleColor,
|
color: number,
|
||||||
order: number,
|
order: number,
|
||||||
): Promise<string | undefined> => {
|
): Promise<string | undefined> => {
|
||||||
const {error} = await manageRelay(url, {
|
const {error} = await manageRelay(url, {
|
||||||
method: "createrole" as ManagementMethod,
|
method: "createrole" as ManagementMethod,
|
||||||
params: [id, label, description, roleColorParams(color), order.toString()],
|
// hue and order go over the wire as JSON numbers, not strings, per the relay's NIP-86 impl.
|
||||||
|
params: [id, label, description, color, order] as unknown as string[],
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (!error) {
|
||||||
|
await reloadRole(url, id)
|
||||||
|
}
|
||||||
|
|
||||||
return error
|
return error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -152,14 +131,19 @@ export const editRole = async (
|
||||||
id: string,
|
id: string,
|
||||||
label: string,
|
label: string,
|
||||||
description: string,
|
description: string,
|
||||||
color: SpaceRoleColor,
|
color: number,
|
||||||
order: number,
|
order: number,
|
||||||
): Promise<string | undefined> => {
|
): Promise<string | undefined> => {
|
||||||
const {error} = await manageRelay(url, {
|
const {error} = await manageRelay(url, {
|
||||||
method: "editrole" as ManagementMethod,
|
method: "editrole" as ManagementMethod,
|
||||||
params: [id, label, description, roleColorParams(color), order.toString()],
|
// hue and order go over the wire as JSON numbers, not strings, per the relay's NIP-86 impl.
|
||||||
|
params: [id, label, description, color, order] as unknown as string[],
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (!error) {
|
||||||
|
await reloadRole(url, id)
|
||||||
|
}
|
||||||
|
|
||||||
return error
|
return error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue