Skip to content

TidaLuna settings page design reworked: better UI & UX in the Tidal design, with better usability and a clearer layout - #197

Draft
FlazeIGuess wants to merge 7 commits into
Inrixia:masterfrom
FlazeIGuess:ui/settings-overhaul
Draft

TidaLuna settings page design reworked: better UI & UX in the Tidal design, with better usability and a clearer layout#197
FlazeIGuess wants to merge 7 commits into
Inrixia:masterfrom
FlazeIGuess:ui/settings-overhaul

Conversation

@FlazeIGuess

@FlazeIGuess FlazeIGuess commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

Requires #195. That PR moves the store list into store/stores.json, and the nightly action in this one reads that file, so this cannot land first.

Opened as a draft. It currently includes #195's commits so it builds and can be reviewed as one piece; once #195 merges I rebase onto master and the diff drops to this PR's own work.

Most people running TidaLuna are not developers. They installed it for one feature they wanted, and the settings page is the only part of it they ever see. It should be understandable without knowing how any of it works, and it should look like it belongs in Tidal rather than sitting on top of it. That is what this is.

It looks like Tidal now

Every colour, radius, surface and easing comes from Tidal's own wave design tokens rather than being approximated. Because Luna references the variables instead of copying the values out, the page follows along when Tidal retunes its palette instead of slowly drifting out of date.

The icons are Tidal's actual icons. Tidal ships no icon font and no third party pack, it mounts its own sprite and draws its whole interface from it, so Luna now draws from the same sprite. @mui/icons-material drops from sixteen imports across nine files to one, the expand chevron in LunaList.tsx.

Settings tab

You can find things

Installed plugins are grouped into errors, enabled and disabled, and move between groups the moment you toggle one, so the list stays organised while you work in it. Search filters as you type.

An expanded plugin

You can see what a plugin is before installing it

Plugin authors can add one preview image, and the store card shows it. Click it and it opens full size.

Cards also show the store's stars, and optionally how many times a plugin has been downloaded. The download counter is off by default and stays off unless the author turns it on, either per plugin or once for a whole store, so nobody has a number put next to their work without choosing to.

Preview image urls are restricted to GitHub hosts. The store draws a card for every plugin of every listed store, so an unrestricted host would let any listed author log the IP of everyone who opens the tab.

Store cards with preview images

Notes for review

The store metrics come from a nightly action that turns stores.json into registry.json, so the client makes one raw fetch rather than hitting the GitHub API per store. Metrics older than seven days are dropped rather than shown stale.

One fix rode along: PluginStoreTab/InstallFromUrl.tsx was a near duplicate of the one in Storage.tsx missing the .css branch its own label promised, so it is gone and the shared one is used.

Bumps the version to 1.17.1-beta, on top of #195's 1.17.0-beta, so the merge produces its own release build.

Checked against a running client throughout rather than only built. Scrolling the store with 80 backdrop-filter layers holds a 6.1ms median frame.

The plugin store list lives in plugins/ui/src/SettingsPage/PluginStoreTab/index.tsx
today, so adding a store needs a code change plus a client release, and removing
one never reaches users who already have it persisted.

store/stores.json is the list instead. Seeded with the 22 stores currently
hardcoded and 7 tombstones recovered from the git history of index.tsx, three of
which are hard 404s that have been showing up as error cards for months.

store/validate.sh checks schema, unknown keys, url and repo consistency,
duplicates and that every active store.json actually resolves with a non empty
plugins array. Needs only jq and curl, runs in about 9 seconds.
@github-actions

github-actions Bot commented Sep 6, 2026

Copy link
Copy Markdown

Flake Review Results for #197

Available systems: aarch64-darwin, aarch64-linux, armv6l-linux, armv7l-linux, i686-linux, powerpc64le-linux, riscv64-linux, x86_64-darwin, x86_64-freebsd, x86_64-linux
Requested systems: aarch64-darwin, x86_64-linux

🔄 Modified (2)

  • packages.aarch64-darwin.default

    • Output: /nix/store/5x4wrk2ga3kq8a7k6fabbbikpvaw38bi-tidal-2.41.3
    Derivation diff
    - /nix/store/w8r83gqv80qc441h1y8pzp9n4yjkrkqx-tidal-2.41.3.drv:{out}
    + /nix/store/qwksz3k952qpw7w5khscsdy8k9p77rvb-tidal-2.41.3.drv:{out}
    • The set of input derivation names do not match:
        - TidaLuna-1.16.6-beta
        + TidaLuna-1.17.1-beta
    • The environments do not match:
        __json=''
        {"NIX_MAIN_PROGRAM":"tidal","__darwinAllowLocalNetworking":false,"__impureHostDeps":["/bin/sh","/usr/lib/libSystem.B.dylib","/usr/lib/system/libunc.dylib","/dev/zero","/dev/random","/dev/urandom","/bin/sh"],"__propagatedImpureHostDeps":[],"__propagatedSandboxProfile":[""],"__sandboxProfile":"","buildInputs":[],"builder":"/nix/store/f700nj7wlwg441h39gkq29qbviy99sgq-bash-5.3p9/bin/bash","cmakeFlags":[],"configureFlags":[],"depsBuildBuild":[],"depsBuildBuildPropagated":[],"depsBuildTarget":[],"depsBuildTargetPropagated":[],"depsHostHost":[],"depsHostHostPropagated":[],"depsTargetTarget":[],"depsTargetTargetPropagated":[],"doCheck":false,"doInstallCheck":false,"dontStrip":true,"env":{"NIX_MAIN_PROGRAM":"tidal"},"installPhase":"runHook preInstall\n\nmkdir -p $out/Applications\ncp -r *.app $out/Applications\n\nrunHook postInstall\n","mesonFlags":[],"name":"tidal-2.41.3","nativeBuildInputs":["/nix/store/q61ydp78wcs21i3aji0pc3xwa78xvd8d-undmg-1.1.0-unstable-2024-08-02"],"outputChecks":{"out":{}},"outputs":["out"],"patches":[],"pname":"tidal","postInstall":"if [ -f \"$out/Applications/TIDAL.app/Contents/Resources/app.asar\" ]; then\n  mv \"$out/Applications/TIDAL.app/Contents/Resources/app.asar\" \\\n     \"$out/Applications/TIDAL.app/Contents/Resources/original.asar\"\nfi\n\nmkdir -p \"$out/Applications/TIDAL.app/Contents/Resources/app/\"\ncp -R /nix/store/bpzav915wd94lspzwc1hrgh8xm9xd8yw-TidaLuna-1.16.6-beta/* \"$out/Applications/TIDAL.app/Contents/Resources/app/\"\n","propagatedBuildInputs":[],"propagatedNativeBuildInputs":[],"sourceRoot":".","src":"/nix/store/sckhn4ryh7jqkdzqi3hz02sgglkl7jwd-TIDAL.arm64.dmg","stdenv":"/nix/store/5qpb391n5rpgdi95j6sczjsplkkgw2v1-stdenv-darwin","strictDeps":true,"system":"aarch64-darwin","version":"2.41.3"}{"NIX_MAIN_PROGRAM":"tidal","__darwinAllowLocalNetworking":false,"__impureHostDeps":["/bin/sh","/usr/lib/libSystem.B.dylib","/usr/lib/system/libunc.dylib","/dev/zero","/dev/random","/dev/urandom","/bin/sh"],"__propagatedImpureHostDeps":[],"__propagatedSandboxProfile":[""],"__sandboxProfile":"","buildInputs":[],"builder":"/nix/store/f700nj7wlwg441h39gkq29qbviy99sgq-bash-5.3p9/bin/bash","cmakeFlags":[],"configureFlags":[],"depsBuildBuild":[],"depsBuildBuildPropagated":[],"depsBuildTarget":[],"depsBuildTargetPropagated":[],"depsHostHost":[],"depsHostHostPropagated":[],"depsTargetTarget":[],"depsTargetTargetPropagated":[],"doCheck":false,"doInstallCheck":false,"dontStrip":true,"env":{"NIX_MAIN_PROGRAM":"tidal"},"installPhase":"runHook preInstall\n\nmkdir -p $out/Applications\ncp -r *.app $out/Applications\n\nrunHook postInstall\n","mesonFlags":[],"name":"tidal-2.41.3","nativeBuildInputs":["/nix/store/q61ydp78wcs21i3aji0pc3xwa78xvd8d-undmg-1.1.0-unstable-2024-08-02"],"outputChecks":{"out":{}},"outputs":["out"],"patches":[],"pname":"tidal","postInstall":"if [ -f \"$out/Applications/TIDAL.app/Contents/Resources/app.asar\" ]; then\n  mv \"$out/Applications/TIDAL.app/Contents/Resources/app.asar\" \\\n     \"$out/Applications/TIDAL.app/Contents/Resources/original.asar\"\nfi\n\nmkdir -p \"$out/Applications/TIDAL.app/Contents/Resources/app/\"\ncp -R /nix/store/n39dpy9srvyrwi91w9lysiz3l8jwjmx9-TidaLuna-1.17.1-beta/* \"$out/Applications/TIDAL.app/Contents/Resources/app/\"\n","propagatedBuildInputs":[],"propagatedNativeBuildInputs":[],"sourceRoot":".","src":"/nix/store/sckhn4ryh7jqkdzqi3hz02sgglkl7jwd-TIDAL.arm64.dmg","stdenv":"/nix/store/5qpb391n5rpgdi95j6sczjsplkkgw2v1-stdenv-darwin","strictDeps":true,"system":"aarch64-darwin","version":"2.41.3"}
    ''
  • packages.x86_64-linux.default

    • Output: /nix/store/8caxv4caxs4psqkl6xa0x375x7r6k3mp-tidal-hifi-6.3.1-Mavy
    Derivation diff
    - /nix/store/js64gd6v64y4rpzhz00q7729n6h7rnyq-tidal-hifi-6.3.1-Mavy.drv:{out}
    + /nix/store/225sqx7frybvwad5b2fww31n9cv0v1bf-tidal-hifi-6.3.1-Mavy.drv:{out}
    • The set of input derivation names do not match:
        - TidaLuna-1.16.6-beta
        + TidaLuna-1.17.1-beta
    • The environments do not match:
        postInstall=''
        mv $out/share/tidal-hifi/resources/app.asar $out/share/tidal-hifi/resources/original.asar
        
        mkdir -p "$out/share/tidal-hifi/resources/app/"
        cp -R /nix/store/zbin7zfshnznw7y1sajwl51x1mgxdysq-TidaLuna-1.16.6-beta/* $out/share/tidal-hifi/resources/app/
        cp -R /nix/store/pd38hrzqjs1wkbw9x2j6h22kr8dx3s0r-TidaLuna-1.17.1-beta/* $out/share/tidal-hifi/resources/app/
    ''

Generated by flake-review

@Solomag

Solomag commented Sep 7, 2026

Copy link
Copy Markdown

Hey! I noticed your ui/settings-overhaul branch while working on Lunatic UI. We are actually solving very similar problems from different sides: plugin store redesign, plugin cards, settings components and TIDAL-style tokens.
I already have a separate UI overhaul plugin with custom Settings/Store/Plugins implementations. Instead of duplicating work, would you be interested in discussing ways to share ideas/components or align approaches?
Your registry/store architecture especially looks interesting because it solves some problems I currently handle locally.

@Inrixia

Inrixia commented Sep 7, 2026

Copy link
Copy Markdown
Owner

Sorry for not having commented on these PR's yet.

I'm not sure if I'll merge the changes for some bits as overhauling the UI is a lot to maintain.

For the stores impl I feel it's a bit overcomplicated (but that's just due to AI being used so heavily).

But when I get time to properly look at things I can talk more on it.

…ng it

Replaces the 22 addToStores calls with a fetch of store/stores.json from raw
githubusercontent, pinned to master so a merged store reaches clients without a
release.

The old storeUrls key mixed defaults and user additions in one list, so removing
a default brought it back on the next start and removing one from the code never
reached anyone who already had it. Split into three: userStoreUrls for what the
user added, hiddenStoreUrls for defaults they dismissed, and the fetched list,
which is persisted and doubles as the offline cache. Tombstoned entries drop dead
stores from clients that already carry them.

The decision logic lives in registry.logic.ts, separate from the reactive state
and the fetching in registry.ts.

Three things found on the way are fixed here because the wiring touched them:
- the deeplink handler lowercased the whole url and pushed it without stripping
  /store.json, persisting a url that always 404s
- Storage.tsx had a second addToStores writing to a different idb store, so
  adding a store from the themes tab silently went nowhere
- LunaStore mapped only the first space in a plugin name and called .filter on
  unvalidated third party json, which takes down the whole settings page
The [master] Release workflow only runs on pushes to master that touch
package.json, so without this the merge would not produce a release build.
Turns store/stores.json into store/registry.json nightly with stars, repo health
and per plugin download counts, so the client makes one raw fetch instead of
hitting the GitHub API once per store. The client tries registry.json and falls
back to stores.json, which satisfies the same shape, so it works before the first
run has happened.

Metrics older than seven days are dropped rather than shown stale, since the
numbers go wrong silently when the generator stops running.
Reads Tidal's wave custom properties rather than approximating them, so the page
follows along when Tidal retunes its palette. One module defines every surface,
border, radius and shadow.

Icons come from Tidal's own sprite. It ships no icon font and no third party pack,
it mounts a sprite of ~108 symbols under body and draws its interface from it with
use href, so Luna draws from the same one and @mui/icons-material leaves the tree.
Six icons the sprite lacks are drawn at the weight the sprite itself uses. The
symbols are not drawn to a common margin, ink runs from 50% to 100% of the box, so
each carries a crop normalising it to the sprite's own 90% median.

Installed plugins are grouped into errors, enabled and disabled and move between
groups as they are toggled. Adding a store moved from a section at the bottom of
the store tab onto a plus beside the search.

A plugin can set image in its package.json and the store card shows it, click to
enlarge. Urls are limited to GitHub hosts: the store renders a card for every
plugin of every listed store, so an unrestricted host would let any listed author
log the IP of everyone who opens the tab. An author avatar goes through the same
check for the same reason. LunaPlugin.refreshPackage comes with it, since
fromStorage returns an instance created earlier in the session without refetching,
so a card pinned whatever the package looked like when it was first rendered.

PluginStoreTab/InstallFromUrl.tsx is removed as a near duplicate of the one in
Storage.tsx, which is the copy that has the .css branch this one's own label
promised. LiveReloadToggle.tsx and LunaPluginHeader.tsx go too, both orphaned by
the rewrite.
This stacks on Inrixia#195, which bumps to 1.17.0-beta. Each merge to master needs its
own package.json touch to trigger the [master] Release build, so this PR carries
the next patch.
One outlined control token now backs both the Select and the TextField
wrappers instead of two near identical copies, so every field shares one
radius, fill and border from a single place.

LunaIcon, LunaSearch, LunaList and LunaPreviewImage lose props and helpers
nothing calls. LunaSetting no longer takes StackProps, so LunaSecureTextSetting
drops the spacing and wrapper it passed for the old Stack layout.

Also disposes the themes list subscription, which was left running when the
tab unmounted.
@FlazeIGuess

FlazeIGuess commented Sep 7, 2026

Copy link
Copy Markdown
Contributor Author

Sorry for not having commented on these PR's yet.

I'm not sure if I'll merge the changes for some bits as overhauling the UI is a lot to maintain.

For the stores impl I feel it's a bit overcomplicated (but that's just due to AI being used so heavily).

But when I get time to properly look at things I can talk more on it.

No rush, and a no on either is a fine answer.

On the store code, that is fair, it was overcomplicated. It was 247 lines across registry.ts and a second file that existed only as an indirection layer so the logic could be unit tested. That file is gone, it is now 146 lines in one file with direct mutations, live on the PR head.

What I am trying to solve with Inrixia/TidaLuna#197: most people running TidaLuna are not developers. They installed it for one feature they wanted, and the settings page is the only part of it they ever see. Right now it asks them to know things they have no reason to know. So the work aims at three things.

Finding what you have. Installed plugins group into errors, enabled and disabled, and move between groups the moment you toggle one. Search filters as you type.

Knowing what you are about to install. A store card can carry one preview image, plus the store's stars and optionally a download count, which stays off unless the author enables it. Before this, deciding whether to install something meant reading a name. Preview urls are limited to four GitHub hosts, since the tab draws a card for every plugin of every listed store, so an open host would let any listed author log the IP of everyone who opens it.

Not looking bolted on. tidalTokens.ts resolves 15 of TIDAL's own CSS custom properties with fallbacks, and the icons are referenced from TIDAL's own sprite rather than a pack. @mui/icons-material goes from sixteen imports across nine files down to one, the chevron in LunaList.tsx, so it stays in the tree.

Inrixia/TidaLuna#195
is separate and smaller than it looks: 13 files, +667/-59, but only +201/-58 of that is client code and 146 of it is the one registry.ts. The rest is the store list, its schema, the validator, the CI check and a README. The tab looks exactly as it does today, only the source of the list moves. The tombstone exists because #184 removed a dead repo and clients built before that merge still fetch it and still show a red error card; that piece and the blocklist can both go without breaking the move.

On maintenance you are right, and the cost lands on you rather than me. I can split
Inrixia/TidaLuna#197
so the shared building blocks (tokens, list and row primitives, icons: 19 files, +938/-66) are one PR and the settings rewiring another. The preview images and the metrics action are the first things I would drop if the surface looks too big.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants