max / makenotwork
28 files changed,
+262 insertions,
-55 deletions
| @@ -92,7 +92,7 @@ | |||
| 92 | 92 | | Section lead | `.section-lead` | with `.mb-section` / `.text-sm` / `.dimmed` utilities | — | | |
| 93 | 93 | | Section divider | `.section-divider` | — | — | | |
| 94 | 94 | | Section grouping label | `.section-group-label` | — | — | | |
| 95 | - | | Badge | `.badge` | status set `.badge--live` / `.badge--pending` / `.badge--failed`; `.free` (price fact); `.ai-tier-*` (disclosure level) | `.is-faded` | | |
| 95 | + | | Badge | `.badge` | status set `.badge--live` / `.badge--pending` / `.badge--failed`, no modifier for a neutral terminal state; `.free` (price fact); `.ai-tier-*` (disclosure level) | `.is-faded` | | |
| 96 | 96 | | Tag | `.tag` (inside `.tag-input` for editing) | — | — | | |
| 97 | 97 | | Callout (solid-tint inline) | `.callout` | `--danger`, `--warning`, `--solid-warning` | — | | |
| 98 | 98 | | Alert (left-border inline) | `.alert` | `-note`, `-tip`, `-important`, `-warning`, `-caution` | — | | |
| @@ -155,7 +155,9 @@ | |||
| 155 | 155 | Hover used to have three options picked by component type, and the second of them was a depth lift: the shadow grew from `var(--shadow-raised)` to `3px 3px var(--shadow-edge)`, reserved for controls that commit a write, so that pressing "Pay" looked different from pressing "Next". That signal is gone rather than moved, and it was spent deliberately. A bevel is an edge and not a distance, so it has no growth axis to extend along; keeping a commit-versus-navigate cue would have meant a local override on top of the `layout.css` every other make-family app takes unmodified. Decided 2026-07-31 (wiki `mnw-platinum`). A control that commits a write is distinguished by its label and its placement, the same way it is in GoingsOn and Balanced Breakfast. | |
| 156 | 156 | - **Focus**: `:focus-visible` shows the `--focus-ring` violet outline. Custom interactive containers (`.card--selectable`, sort headers) must opt in by adding `:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 2px; }`. A container the browser does not focus on its own also needs `tabindex="0"` in the markup, or the rule never matches and the outline is decoration for a state that cannot happen. The `data-action` dispatcher activates such a container on Enter and Space. | |
| 157 | 157 | - **Selected / active**: `.is-selected` modifier applies `background: var(--highlight-faint)` plus the focus-ring border. `.tab.active`, `.filter-item.active`, `.view-btn.active`, and the `:checked + .card--selectable-inner` recipe all read it. | |
| 158 | - | - **Status**: a badge names the status it reports, and the colour follows from the status. The set is `.badge--live` (Enabled, Active, Available, Connected, Current), `.badge--pending` (Pending, Draft) and `.badge--failed` (Failed, Suspended). A badge reports data, not selection, so it never takes `.is-selected` and there is no `.badge.active`. A class that names a colour rather than a state is the same category error as `.mb-4` naming a number. `.free` and the `.ai-tier-*` trio stay outside the set: a price fact and a disclosure level are not lifecycle. | |
| 158 | + | - **Status**: a badge names the status it reports, and the colour follows from the status. The set is `.badge--live` (Active, Trialing, Completed, Approved, Published, Connected), `.badge--pending` (Pending, Processing, Scheduled, Draft, Incomplete) and `.badge--failed` (Failed, Past due, Unpaid, Suspended, Spam). A terminal state nobody has to act on (Refunded, Canceled) takes the plain `.badge` with no modifier, which already reads as neutral: colouring it red would ask for an action that does not exist. A badge reports data, not selection, so it never takes `.is-selected` and there is no `.badge.active`. A class that names a colour rather than a state is the same category error as `.mb-4` naming a number. `.free` and the `.ai-tier-*` trio stay outside the set: a price fact and a disclosure level are not lifecycle. | |
| 159 | + | ||
| 160 | + | The class is chosen in Rust, by `BadgeStatus` in `src/types/badge.rs`, and reaches the template as a `status_class` field. A template never interpolates a status column into a class attribute. That habit made the stylesheet mirror whatever the schema spelled, so a column rename unstyled the badge and a status nobody had written a rule for rendered plain, which is what "Scheduled" and every subscription status other than `active` did until 2026-08-02. Each status enum maps into the set with an exhaustive match, so a new variant is a compile error. | |
| 159 | 161 | - **Disabled**: `:disabled` and `[aria-disabled="true"]` show `opacity: 0.5` and `cursor: not-allowed`. | |
| 160 | 162 | - **Busy / loading**: HTMX-driven via `.htmx-request` on the trigger. There is no skeleton primitive: one was documented here for months and never built, so it was removed rather than left as a promise. | |
| 161 | 163 |
| @@ -80,7 +80,7 @@ | |||
| 80 | 80 | 6. **Forms** (`.form-group`, `.form-section`, `.checkbox-group`) | |
| 81 | 81 | 7. **Tables** (`.data-table`, `.compact-table`) | |
| 82 | 82 | 8. **Utilities** (`.text-sm`, `.muted`, `.scroll-x`, etc.) | |
| 83 | - | 9. **Badges** (`.badge`, `.badge-success`, `.badge-danger`) | |
| 83 | + | 9. **Badges** (`.badge` plus the status set `.badge--live` / `.badge--pending` / `.badge--failed`) | |
| 84 | 84 | 10. **Cards** (`.card`, `.stat-card`, `.project-card`) | |
| 85 | 85 | 11. **Navigation** (`.tabs`, `.tab`, header/footer) | |
| 86 | 86 | 12. **Components** (modals, toasts, progress bars, onboarding) |
| @@ -1019,27 +1019,6 @@ | |||
| 1019 | 1019 | background: var(--danger); | |
| 1020 | 1020 | } | |
| 1021 | 1021 | ||
| 1022 | - | /* Raw-status classes. Several templates interpolate a status column straight | |
| 1023 | - | into the class attribute (`class="badge {{ tx.status|lowercase }}"`), so these | |
| 1024 | - | names are written by the data rather than chosen by an author. GoingsOn task | |
| 1025 | - | 8a007e42 moves that behind a status_class() helper on the view model, which | |
| 1026 | - | returns a name from the set above; this block goes when that lands. */ | |
| 1027 | - | .badge.active, | |
| 1028 | - | .badge.current { | |
| 1029 | - | background: var(--success); | |
| 1030 | - | } | |
| 1031 | - | ||
| 1032 | - | .badge.pending, | |
| 1033 | - | .badge.draft, | |
| 1034 | - | .badge.suppressed { | |
| 1035 | - | background: var(--warning); | |
| 1036 | - | } | |
| 1037 | - | ||
| 1038 | - | .badge.failed, | |
| 1039 | - | .badge.suspended { | |
| 1040 | - | background: var(--danger); | |
| 1041 | - | } | |
| 1042 | - | ||
| 1043 | 1022 | .badge.free { | |
| 1044 | 1023 | background: var(--action); | |
| 1045 | 1024 | color: var(--primary-light); | |
| @@ -2585,18 +2564,6 @@ | |||
| 2585 | 2564 | font-weight: bold; | |
| 2586 | 2565 | } | |
| 2587 | 2566 | ||
| 2588 | - | .import-page .status-badge { | |
| 2589 | - | display: inline-block; | |
| 2590 | - | padding: var(--gap-bound) var(--gap-peer); | |
| 2591 | - | font-size: var(--text-fine); | |
| 2592 | - | font-family: var(--font-mono); | |
| 2593 | - | } | |
| 2594 | - | ||
| 2595 | - | .import-page .status-badge.completed { color: var(--success); } | |
| 2596 | - | .import-page .status-badge.processing { color: var(--action); } | |
| 2597 | - | .import-page .status-badge.failed { color: var(--danger); } | |
| 2598 | - | .import-page .status-badge.pending { opacity: 0.6; } | |
| 2599 | - | ||
| 2600 | 2567 | .import-page .import-note { | |
| 2601 | 2568 | background: var(--surface-sunken); | |
| 2602 | 2569 | padding: var(--gap-section); |
| @@ -142,6 +142,18 @@ | |||
| 142 | 142 | Spam => "spam", | |
| 143 | 143 | }); | |
| 144 | 144 | ||
| 145 | + | impl WaitlistStatus { | |
| 146 | + | /// Badge vocabulary (charter: `docs/design-system.md`). | |
| 147 | + | pub fn badge_status(self) -> crate::types::BadgeStatus { | |
| 148 | + | use crate::types::BadgeStatus; | |
| 149 | + | match self { | |
| 150 | + | Self::Approved => BadgeStatus::Live, | |
| 151 | + | Self::Pending => BadgeStatus::Pending, | |
| 152 | + | Self::Spam => BadgeStatus::Failed, | |
| 153 | + | } | |
| 154 | + | } | |
| 155 | + | } | |
| 156 | + | ||
| 145 | 157 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] | |
| 146 | 158 | pub enum SelectionMethod { | |
| 147 | 159 | #[serde(rename = "hand_picked")] | |
| @@ -186,6 +198,20 @@ | |||
| 186 | 198 | Failed => "failed", | |
| 187 | 199 | }); | |
| 188 | 200 | ||
| 201 | + | impl TransactionStatus { | |
| 202 | + | /// Badge vocabulary (charter: `docs/design-system.md`). A refund is over | |
| 203 | + | /// and needs nobody, so it is neutral rather than red. | |
| 204 | + | pub fn badge_status(self) -> crate::types::BadgeStatus { | |
| 205 | + | use crate::types::BadgeStatus; | |
| 206 | + | match self { | |
| 207 | + | Self::Completed => BadgeStatus::Live, | |
| 208 | + | Self::Pending | Self::Refunding => BadgeStatus::Pending, | |
| 209 | + | Self::Failed => BadgeStatus::Failed, | |
| 210 | + | Self::Refunded => BadgeStatus::Ended, | |
| 211 | + | } | |
| 212 | + | } | |
| 213 | + | } | |
| 214 | + | ||
| 189 | 215 | // ── Follows ── | |
| 190 | 216 | ||
| 191 | 217 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] | |
| @@ -232,6 +258,21 @@ | |||
| 232 | 258 | Unpaid => "unpaid", | |
| 233 | 259 | }); | |
| 234 | 260 | ||
| 261 | + | impl SubscriptionStatus { | |
| 262 | + | /// Badge vocabulary (charter: `docs/design-system.md`). A trial is live | |
| 263 | + | /// because the subscriber has access; a cancellation is over and needs | |
| 264 | + | /// nobody, so it is neutral rather than red. | |
| 265 | + | pub fn badge_status(self) -> crate::types::BadgeStatus { | |
| 266 | + | use crate::types::BadgeStatus; | |
| 267 | + | match self { | |
| 268 | + | Self::Active | Self::Trialing => BadgeStatus::Live, | |
| 269 | + | Self::Incomplete => BadgeStatus::Pending, | |
| 270 | + | Self::IncompleteExpired | Self::PastDue | Self::Unpaid => BadgeStatus::Failed, | |
| 271 | + | Self::Canceled => BadgeStatus::Ended, | |
| 272 | + | } | |
| 273 | + | } | |
| 274 | + | } | |
| 275 | + | ||
| 235 | 276 | // ── SyncKit developer billing ── | |
| 236 | 277 | ||
| 237 | 278 | /// Lifecycle of a SyncKit developer app's billing record (the `sync_apps.billing_status` | |
| @@ -647,6 +688,17 @@ | |||
| 647 | 688 | Self::Generated => "Generated", | |
| 648 | 689 | } | |
| 649 | 690 | } | |
| 691 | + | ||
| 692 | + | /// The badge modifier for this tier. A disclosure level is not lifecycle, | |
| 693 | + | /// so it keeps its own names rather than joining the status set, but the | |
| 694 | + | /// class still comes from here rather than from the serialized value. | |
| 695 | + | pub fn css_class(&self) -> &'static str { | |
| 696 | + | match self { | |
| 697 | + | Self::Handmade => "ai-tier-handmade", | |
| 698 | + | Self::Assisted => "ai-tier-assisted", | |
| 699 | + | Self::Generated => "ai-tier-generated", | |
| 700 | + | } | |
| 701 | + | } | |
| 650 | 702 | } | |
| 651 | 703 | ||
| 652 | 704 | /// Discover-page filter shape per `about/generative-ai.md` § "How Fans | |
| @@ -1042,6 +1094,18 @@ | |||
| 1042 | 1094 | Failed => "failed", | |
| 1043 | 1095 | }); | |
| 1044 | 1096 | ||
| 1097 | + | impl ImportJobStatus { | |
| 1098 | + | /// Badge vocabulary (charter: `docs/design-system.md`). | |
| 1099 | + | pub fn badge_status(self) -> crate::types::BadgeStatus { | |
| 1100 | + | use crate::types::BadgeStatus; | |
| 1101 | + | match self { | |
| 1102 | + | Self::Completed => BadgeStatus::Live, | |
| 1103 | + | Self::Pending | Self::Processing => BadgeStatus::Pending, | |
| 1104 | + | Self::Failed => BadgeStatus::Failed, | |
| 1105 | + | } | |
| 1106 | + | } | |
| 1107 | + | } | |
| 1108 | + | ||
| 1045 | 1109 | // Moderation action types | |
| 1046 | 1110 | ||
| 1047 | 1111 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] |
| @@ -265,6 +265,8 @@ | |||
| 265 | 265 | pub struct ImportJobRow { | |
| 266 | 266 | pub source: String, | |
| 267 | 267 | pub status: String, | |
| 268 | + | /// Badge class for `status`, from `ImportJobStatus::badge_status`. | |
| 269 | + | pub status_class: &'static str, | |
| 268 | 270 | pub total_rows: i32, | |
| 269 | 271 | pub created_rows: i32, | |
| 270 | 272 | pub created_at: chrono::DateTime<chrono::Utc>, |
| @@ -13,6 +13,8 @@ | |||
| 13 | 13 | pub email_verified: bool, | |
| 14 | 14 | pub pitch: Option<String>, | |
| 15 | 15 | pub status: String, | |
| 16 | + | /// Badge class for `status`, from `WaitlistStatus::badge_status`. | |
| 17 | + | pub status_class: &'static str, | |
| 16 | 18 | pub selection_method: Option<String>, | |
| 17 | 19 | pub admin_note: Option<String>, | |
| 18 | 20 | pub created_at: String, |
| @@ -15,5 +15,7 @@ | |||
| 15 | 15 | pub title: String, | |
| 16 | 16 | pub slug: String, | |
| 17 | 17 | pub status: String, | |
| 18 | + | /// Badge class for `status`, picked beside the label. | |
| 19 | + | pub status_class: &'static str, | |
| 18 | 20 | pub published_at: String, | |
| 19 | 21 | } |