refactor(commons): move feature-specific UI out of ui/ into <feature>/ui

Make the feature-UI vs cross-cutting-UI rule consistent (feature-first):
- ui/nip53LiveActivities -> nip53LiveActivities/ui
- ui/article + ui/editor -> new nip23LongContent/ui (article reader + editor)

ui/ now holds only cross-cutting composables (theme, components, layouts,
elements, markdown, signing, thread, feeds, notifications, screens, state,
text). Tighten ARCHITECTURE.md with the deciding test ('could a second
unrelated feature reuse this as-is?') and reconcile the NIP-second-axis
section so a single-NIP feature owns its UI under <feature>/ui rather than
ui/nipNN.

https://claude.ai/code/session_01KXLzsvx9Gyrm3Yz4Rims55
This commit is contained in:
Claude
2026-05-30 19:02:55 +00:00
parent 79a9bf78f8
commit d8899eedf8
12 changed files with 36 additions and 25 deletions
@@ -35,7 +35,7 @@ import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.commons.ui.nip53LiveActivities.StreamSystemCard
import com.vitorpamplona.amethyst.commons.nip53LiveActivities.ui.StreamSystemCard
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.service.playback.composable.VideoView
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
@@ -37,7 +37,7 @@ import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.commons.icons.symbols.Icon
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
import com.vitorpamplona.amethyst.commons.ui.nip53LiveActivities.StreamSystemCard
import com.vitorpamplona.amethyst.commons.nip53LiveActivities.ui.StreamSystemCard
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.ui.navigation.routes.Route
@@ -36,7 +36,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.commons.ui.nip53LiveActivities.StreamSystemCard
import com.vitorpamplona.amethyst.commons.nip53LiveActivities.ui.StreamSystemCard
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.ui.note.CrossfadeToDisplayComment
+21 -10
View File
@@ -78,7 +78,7 @@ package contains Compose UI (and is therefore *not* CLI-safe).
| `account` | no | New-account bootstrap events. |
| `onchain` | no | On-chain zap splitting/broadcasting. |
| `marmot` | no | MLS group-chat event processing. |
| `nip53LiveActivities` | no | Live-activity zapper aggregation. |
| `nip53LiveActivities` | mixed | Live-activity zapper aggregation (logic) + the stream card in `nip53LiveActivities/ui`. |
| `search` | no | Event search filtering/ranking, kind registry. |
| `preview` | no | OpenGraph / meta-tag link-preview parsing. |
| `emojicoder` | no | Variation-selector emoji encode/decode. |
@@ -116,7 +116,8 @@ they are shared across the GUI apps. Treat as GUI-shared, not strictly headless.
### UI (Compose — **not** CLI-safe)
| Package | UI? | Purpose |
|----------------|-----|---------|
| `ui` | yes | All shared composables, organized by area: `ui/components`, `ui/theme`, `ui/signing`, `ui/thread`, `ui/feeds` (feed DAL + filters — see debt §4), `ui/notifications`, `ui/screens`, `ui/article`, `ui/editor`, `ui/elements`, `ui/layouts`, `ui/markdown`, `ui/nip53LiveActivities`, plus Compose helpers in `ui/state` (cached-state) and `ui/text` (TextField extensions). |
| `ui` | yes | **Cross-cutting** shared composables only, organized by area: `ui/components`, `ui/theme`, `ui/signing`, `ui/thread`, `ui/feeds` (feed DAL + filters — see debt §4), `ui/notifications`, `ui/screens`, `ui/elements`, `ui/layouts`, `ui/markdown`, plus Compose helpers in `ui/state` (cached-state) and `ui/text` (TextField extensions). Feature-specific UI lives in `<feature>/ui`, **not** here. |
| `nip23LongContent` | yes | Long-form (NIP-23) article UI: `nip23LongContent/ui/article` (reader) + `…/ui/editor` (authoring). The model lives in `model/nip23LongContent`. |
| `icons` | yes | `ImageVector` icon definitions + builders. |
| `hashtags` | yes | Custom hashtag `ImageVector`s. |
| `robohash` | yes | Procedural robohash avatar `ImageVector` assembly. |
@@ -146,8 +147,14 @@ share them.
### Naming
- **Singular, no synonyms.** `util` (not `utils`), `service` (not `services`).
One concept → one package name.
- Composables that belong to a feature go in `<feature>/ui`; cross-cutting
composables go under `ui/<area>`.
- **Feature UI vs cross-cutting UI — the deciding test.** A composable goes in
`<feature>/ui` if it renders/edits *one* feature's content (it would make no
sense outside that feature) — e.g. `profile/ui`, `nip53LiveActivities/ui`,
`nip23LongContent/ui`. It goes in `ui/<area>` only if it is reusable across
features (theme, avatars, buttons, layouts, markdown rendering, shimmer…).
When in doubt, ask "could a second, unrelated feature reuse this as-is?" —
yes → `ui/<area>`, no → `<feature>/ui`. The top-level `ui/` package holds
**no** feature-specific composables.
### NIP as the second axis (mirror `quartz`)
`quartz` is ~94% organized by NIP (`nipNN<slug>` per spec), and that is correct
@@ -160,16 +167,20 @@ problem one level down.
Instead, **layer is the primary axis, NIP is the secondary axis**:
- The big shared layers stay layer-organized (`model`, `ui`, `relayClient`, …).
- The big shared layers stay layer-organized (`model`, `relayClient`, the
cross-cutting `ui`, …).
- **Inside a layer, NIP-specific code goes in a `nipNN<slug>` subpackage whose
name matches `quartz` exactly** — e.g. `model/nip57Zaps`, `ui/nip53LiveActivities`.
This gives a clean trace: `quartz/nip57Zaps` → `commons/model/nip57Zaps` →
`commons/ui/nip57Zaps`.
name matches `quartz` exactly** — e.g. `model/nip57Zaps`. This gives a clean
trace: `quartz/nip57Zaps` → `commons/model/nip57Zaps`.
- **A top-level package that *is* a single self-contained NIP feature takes the
same name as its `quartz` counterpart**: `nip64Chess`, `nipACWebRtcCalls`,
`nip53LiveActivities`, `marmot`. (`marmot` is un-numbered in `quartz` too.)
same name as its `quartz` counterpart**, and owns its own UI under
`<feature>/ui`: `nip64Chess`, `nipACWebRtcCalls`, `nip53LiveActivities`,
`nip23LongContent`, `marmot`. (`marmot` is un-numbered in `quartz` too.)
Generic/multi-NIP packages keep their concern name (`search`, `preview`,
`actions`, `richtext`…).
- **Feature UI is never under `ui/`.** A single-NIP feature's composables live
in `<feature>/ui` (e.g. `nip53LiveActivities/ui`), not `ui/nip53LiveActivities`.
`ui/` is exclusively cross-cutting (§2, Naming).
### Source sets
| Source set | For |
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.commons.ui.article
package com.vitorpamplona.amethyst.commons.nip23LongContent.ui.article
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.commons.ui.article
package com.vitorpamplona.amethyst.commons.nip23LongContent.ui.article
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.commons.ui.editor
package com.vitorpamplona.amethyst.commons.nip23LongContent.ui.editor
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.commons.ui.editor
package com.vitorpamplona.amethyst.commons.nip23LongContent.ui.editor
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.commons.ui.editor
package com.vitorpamplona.amethyst.commons.nip23LongContent.ui.editor
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.commons.ui.nip53LiveActivities
package com.vitorpamplona.amethyst.commons.nip53LiveActivities.ui
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
@@ -65,9 +65,9 @@ import androidx.compose.ui.unit.sp
import com.vitorpamplona.amethyst.commons.icons.symbols.Icon
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
import com.vitorpamplona.amethyst.commons.model.nip23LongContent.LongFormPublishAction
import com.vitorpamplona.amethyst.commons.ui.editor.MarkdownEditorState
import com.vitorpamplona.amethyst.commons.ui.editor.MarkdownToolbar
import com.vitorpamplona.amethyst.commons.ui.editor.MetadataPanel
import com.vitorpamplona.amethyst.commons.nip23LongContent.ui.editor.MarkdownEditorState
import com.vitorpamplona.amethyst.commons.nip23LongContent.ui.editor.MarkdownToolbar
import com.vitorpamplona.amethyst.commons.nip23LongContent.ui.editor.MetadataPanel
import com.vitorpamplona.amethyst.commons.ui.markdown.RenderMarkdown
import com.vitorpamplona.amethyst.desktop.account.AccountState
import com.vitorpamplona.amethyst.desktop.network.DesktopRelayConnectionManager
@@ -69,9 +69,9 @@ import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.commons.icons.symbols.Icon
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
import com.vitorpamplona.amethyst.commons.model.nip23LongContent.ReadingTimeCalculator
import com.vitorpamplona.amethyst.commons.ui.article.ArticleHeader
import com.vitorpamplona.amethyst.commons.ui.article.TableOfContents
import com.vitorpamplona.amethyst.commons.ui.article.extractTableOfContents
import com.vitorpamplona.amethyst.commons.nip23LongContent.ui.article.ArticleHeader
import com.vitorpamplona.amethyst.commons.nip23LongContent.ui.article.TableOfContents
import com.vitorpamplona.amethyst.commons.nip23LongContent.ui.article.extractTableOfContents
import com.vitorpamplona.amethyst.commons.ui.components.EmptyState
import com.vitorpamplona.amethyst.commons.ui.components.LoadingState
import com.vitorpamplona.amethyst.commons.ui.markdown.RenderMarkdown