Skip to main content

bucketwarden_browser_ui_design/
lib.rs

1use serde::{Deserialize, Serialize};
2
3pub const CRATE_PURPOSE: &str = "browser ui design tokens and css contracts";
4
5#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
6pub struct BrowserUiToken {
7    pub name: String,
8    pub value: String,
9    pub feature_id: String,
10}
11
12#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
13pub struct BrowserUiDesignSystem {
14    pub boundary_id: String,
15    pub implementation_status: String,
16    pub tokens: Vec<BrowserUiToken>,
17    pub component_contracts: Vec<String>,
18}
19
20pub fn browser_ui_design_system() -> BrowserUiDesignSystem {
21    BrowserUiDesignSystem {
22        boundary_id: "bnd:bucketwarden.browser-ui.foundation-slice".to_string(),
23        implementation_status: "implemented".to_string(),
24        tokens: browser_ui_tokens(),
25        component_contracts: vec![
26            "tables".to_string(),
27            "drawers".to_string(),
28            "modals".to_string(),
29            "tabs".to_string(),
30            "badges".to_string(),
31            "alerts".to_string(),
32            "forms".to_string(),
33            "focus states".to_string(),
34        ],
35    }
36}
37
38pub fn browser_ui_tokens() -> Vec<BrowserUiToken> {
39    [
40        (
41            "--color-bg-app",
42            "#f4f7f9",
43            "feat:bucketwarden.ui.browser.tokens.color",
44        ),
45        (
46            "--color-bg-surface",
47            "#ffffff",
48            "feat:bucketwarden.ui.browser.tokens.color",
49        ),
50        (
51            "--color-bg-sidebar",
52            "#13232d",
53            "feat:bucketwarden.ui.browser.tokens.color",
54        ),
55        (
56            "--color-text-primary",
57            "#17212b",
58            "feat:bucketwarden.ui.browser.tokens.color",
59        ),
60        (
61            "--color-text-muted",
62            "#5c6670",
63            "feat:bucketwarden.ui.browser.tokens.color",
64        ),
65        (
66            "--color-border-subtle",
67            "#d8dde3",
68            "feat:bucketwarden.ui.browser.tokens.color",
69        ),
70        (
71            "--color-action-primary",
72            "#176b87",
73            "feat:bucketwarden.ui.browser.tokens.color",
74        ),
75        (
76            "--color-status-success",
77            "#247a4b",
78            "feat:bucketwarden.ui.browser.tokens.focus-status",
79        ),
80        (
81            "--color-status-warning",
82            "#a35a00",
83            "feat:bucketwarden.ui.browser.tokens.focus-status",
84        ),
85        (
86            "--color-status-danger",
87            "#a53333",
88            "feat:bucketwarden.ui.browser.tokens.focus-status",
89        ),
90        (
91            "--space-1",
92            "4px",
93            "feat:bucketwarden.ui.browser.tokens.spacing",
94        ),
95        (
96            "--space-2",
97            "8px",
98            "feat:bucketwarden.ui.browser.tokens.spacing",
99        ),
100        (
101            "--space-3",
102            "12px",
103            "feat:bucketwarden.ui.browser.tokens.spacing",
104        ),
105        (
106            "--space-4",
107            "16px",
108            "feat:bucketwarden.ui.browser.tokens.spacing",
109        ),
110        (
111            "--radius-1",
112            "4px",
113            "feat:bucketwarden.ui.browser.tokens.table",
114        ),
115        (
116            "--radius-2",
117            "8px",
118            "feat:bucketwarden.ui.browser.tokens.table",
119        ),
120        (
121            "--font-size-sm",
122            "14px",
123            "feat:bucketwarden.ui.browser.tokens.typography",
124        ),
125        (
126            "--font-size-md",
127            "16px",
128            "feat:bucketwarden.ui.browser.tokens.typography",
129        ),
130        (
131            "--table-row-height",
132            "40px",
133            "feat:bucketwarden.ui.browser.tokens.table",
134        ),
135        (
136            "--focus-ring",
137            "3px solid #88c8dc",
138            "feat:bucketwarden.ui.browser.tokens.focus-status",
139        ),
140    ]
141    .into_iter()
142    .map(|(name, value, feature_id)| BrowserUiToken {
143        name: name.to_string(),
144        value: value.to_string(),
145        feature_id: feature_id.to_string(),
146    })
147    .collect()
148}
149
150pub fn browser_ui_css() -> &'static str {
151    r#":root{color-scheme:light;--color-bg-app:#f4f7f9;--color-bg-surface:#fff;--color-bg-sidebar:#13232d;--color-text-primary:#17212b;--color-text-muted:#5c6670;--color-border-subtle:#d8dde3;--color-action-primary:#176b87;--color-status-success:#247a4b;--color-status-warning:#a35a00;--color-status-danger:#a53333;--space-1:4px;--space-2:8px;--space-3:12px;--space-4:16px;--radius-1:4px;--radius-2:8px;--font-size-sm:14px;--font-size-md:16px;--table-row-height:40px;--focus-ring:3px solid #88c8dc;font-family:Inter,Segoe UI,Arial,sans-serif}*{box-sizing:border-box}[hidden]{display:none!important}body{margin:0;color:var(--color-text-primary);background:var(--color-bg-app)}#app{min-height:100vh;display:grid;grid-template-columns:256px 1fr}.shell-nav{background:var(--color-bg-sidebar);color:#fff;padding:20px}.brand{display:flex;align-items:center;gap:10px;color:#fff;text-decoration:none;font-size:20px;font-weight:800;line-height:1;margin:0 0 10px}.brand img{width:42px;height:42px;object-fit:contain;flex:0 0 auto}.login-mark{width:74px;height:74px;object-fit:contain;display:block;margin:0 0 14px}.principal{color:#c3dbe4;margin:0 0 18px}.shell-nav nav{display:grid;gap:6px}.shell-nav a{color:#dcecf2;text-decoration:none;padding:9px 10px;border-radius:var(--radius-2)}.shell-nav a.is-active,.shell-nav a:focus,.shell-nav a:hover{outline:2px solid #9bd6e8;background:#203744}main{padding:24px;display:grid;gap:18px;align-content:start}.topbar{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:14px;align-items:start}.route-kicker{margin:0;color:var(--color-text-muted);font-size:var(--font-size-sm);font-weight:700}.topbar h1{font-size:28px;line-height:1.15;margin:2px 0 4px}.route-summary{margin:0;color:var(--color-text-muted)}.context-strip{display:flex;gap:8px;flex-wrap:wrap;margin-top:10px}.context-chip{border:1px solid var(--color-border-subtle);border-radius:var(--radius-2);background:#fff;padding:6px 8px;font-size:var(--font-size-sm);font-weight:700}.refresh-controls{display:grid;grid-template-columns:auto minmax(96px,130px);gap:8px;align-items:end}.refresh-controls button{margin-top:0}.action-filter-bar{display:grid;grid-template-columns:minmax(180px,1.3fr) minmax(140px,1fr) minmax(120px,.8fr) minmax(140px,.9fr) minmax(120px,.7fr) auto auto;gap:10px;align-items:end;background:#fff;border:1px solid var(--color-border-subtle);border-radius:var(--radius-2);padding:12px}.field-control{display:grid;gap:5px;margin:0}.field-control span{font-size:12px;text-transform:uppercase;letter-spacing:.04em;color:var(--color-text-muted)}.filter-scope{align-self:center;color:var(--color-text-muted);font-size:var(--font-size-sm);font-weight:700}.dashboard{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:14px}.panel,.state-panel{background:var(--color-bg-surface);border:1px solid var(--color-border-subtle);border-radius:var(--radius-2);padding:var(--space-4)}.panel[data-route-panel=login]{max-width:560px;width:100%;margin:28px auto 0;padding:28px}.panel h2{font-size:18px;margin:0 0 10px}.panel[data-route-panel=login] h2{font-size:22px;margin-bottom:14px}.toolbar{display:grid;grid-template-columns:repeat(3,minmax(140px,1fr)) auto;gap:10px;align-items:end;margin-bottom:12px}.explorer-shell{display:grid;grid-template-columns:minmax(260px,320px) minmax(0,1fr);gap:16px;align-items:start}.file-explorer-grid{display:grid;grid-template-columns:minmax(220px,280px) minmax(0,1fr);gap:12px}.bucket-tabs{display:flex;gap:0;margin:8px 0 12px;border-bottom:1px solid var(--color-border-subtle)}.bucket-tabs [role=tab]{border:1px solid transparent;border-bottom:0;border-radius:6px 6px 0 0;background:transparent;color:var(--color-text-primary);margin:0 0 -1px;padding:9px 14px}.bucket-tabs [aria-selected=true]{background:#fff;border-color:var(--color-border-subtle);box-shadow:inset 0 3px 0 var(--color-action-primary)}.breadcrumbs{display:flex;align-items:center;gap:4px;flex-wrap:wrap;margin:8px 0 12px}.breadcrumb-item{margin:0;border:0;background:transparent;color:var(--color-action-primary);padding:4px 2px;font-weight:700}.breadcrumb-separator{color:var(--color-text-muted)}.preview-panel{border:1px solid var(--color-border-subtle);border-radius:var(--radius-2);background:#fbfcfd;padding:12px;color:var(--color-text-muted)}.content-inspector{display:grid;grid-template-columns:minmax(0,1fr) 280px;gap:16px}.secondary-inspector{position:sticky;top:18px;align-self:start;background:#fbfcfd;border:1px solid var(--color-border-subtle);border-radius:var(--radius-2);padding:var(--space-4)}.secondary-inspector.is-collapsed .inspector-list{display:none}.secondary-inspector.is-collapsed{max-height:72px;overflow:hidden}.inspector-head{display:flex;align-items:center;justify-content:space-between;gap:8px}.secondary-inspector h2{font-size:16px;margin:0}.inspector-list{grid-template-columns:1fr;gap:4px}.explorer{border:1px solid var(--color-border-subtle);border-radius:var(--radius-2);background:#fbfcfd;min-height:220px;padding:8px}.explorer ul{list-style:none;margin:0;padding:0}.explorer li{margin:2px 0}.explorer button{width:100%;margin:0;background:transparent;color:var(--color-text-primary);text-align:left;border:1px solid transparent;border-radius:var(--radius-1);font-weight:600;cursor:pointer}.explorer button:hover,.explorer button:focus{background:#eaf3f6;border-color:#9bd6e8}.explorer button.is-selected,.explorer button[aria-pressed=true]{background:#dcecf2;border-color:var(--color-action-primary);box-shadow:inset 3px 0 0 var(--color-action-primary)}.entity-button{display:grid;grid-template-columns:22px minmax(0,1fr);gap:6px;align-items:center}.entity-icon{font-size:16px;text-align:center}.entity-name{overflow-wrap:anywhere}.entity-meta{grid-column:2;color:var(--color-text-muted);font-size:var(--font-size-sm);font-weight:600}.object-node{padding-left:18px}.governance-gates{display:flex;gap:8px;flex-wrap:wrap;margin:10px 0 14px}.governance-gates button{margin-top:0}.is-gated{border-style:dashed}.empty-state{border:1px dashed var(--color-border-subtle);border-radius:var(--radius-2);color:var(--color-text-muted);padding:14px;background:#fbfcfd}dl{display:grid;grid-template-columns:minmax(120px,180px) 1fr;gap:6px 12px}dt{color:var(--color-text-muted)}dd{margin:0;font-weight:700;overflow-wrap:anywhere}table{width:100%;border-collapse:collapse}th,td{border-bottom:1px solid var(--color-border-subtle);min-height:var(--table-row-height);padding:8px;text-align:left;vertical-align:top;overflow-wrap:anywhere}label{display:block;font-weight:600;margin-top:10px}input,select{width:100%;min-height:38px;border:1px solid var(--color-border-subtle);border-radius:6px;padding:8px;background:#fff}button{margin-top:12px;min-height:38px;border:0;border-radius:6px;background:var(--color-action-primary);color:#fff;padding:8px 14px;font-weight:700}.secondary-action{border:1px solid var(--color-border-subtle);background:#fff;color:var(--color-text-primary)}button:disabled{opacity:.64;cursor:not-allowed}button:focus,input:focus,select:focus,.brand:focus{outline:var(--focus-ring);outline-offset:2px}[role=status]{color:var(--color-text-muted)}[data-denied]{color:var(--color-status-danger);font-weight:700}.is-error{color:var(--color-status-danger)}.is-ok{color:var(--color-status-success)}body[data-route-chrome=login] #app{grid-template-columns:1fr}body[data-route-chrome=login] .shell-nav{display:none}body[data-route-chrome=login] .topbar{display:none}body[data-route-chrome=login] main{min-height:100vh;place-content:center;background:linear-gradient(180deg,#eef4f7 0,#f8fafb 100%)}body[data-route-chrome=login] .content-inspector,body[data-inspector-visible=false] .content-inspector{grid-template-columns:1fr}.overview-kpi-dashboard{grid-template-columns:1fr}.overview-summary{max-width:980px}.kpi-grid{grid-template-columns:repeat(4,minmax(130px,1fr));gap:10px}.kpi-grid dt{font-size:12px;text-transform:uppercase;letter-spacing:.04em}.kpi-grid dd{font-size:24px;line-height:1.1}.explorer-region{min-width:0}.explorer-region>h3{margin:0 0 8px}.bucket-list .entity-icon{color:#176b87}.file-tree .entity-icon{color:#a35a00}.object-table .entity-icon{color:#247a4b}.bucket-tabs [role=tab]{appearance:none;min-height:40px;margin:0 0 -1px;background:#f8fafb;color:var(--color-text-muted)}.bucket-tabs [role=tab]:hover{background:#eef4f7}.bucket-tabs [aria-selected=true]{color:var(--color-text-primary);font-weight:800}.breadcrumbs{border:1px solid var(--color-border-subtle);border-radius:var(--radius-2);background:#fbfcfd;padding:7px 10px}.breadcrumb-item[aria-current=page]{color:var(--color-text-primary);cursor:default}.gate-control{display:grid;gap:4px}.gate-note{font-size:12px;color:var(--color-text-muted)}.secondary-inspector[hidden]{display:none!important}@media(max-width:1180px){.content-inspector,.explorer-shell,.file-explorer-grid{grid-template-columns:1fr}.secondary-inspector{position:relative;top:auto}.action-filter-bar{grid-template-columns:repeat(2,minmax(0,1fr))}.filter-scope{grid-column:1/-1}}@media(max-width:760px){#app{grid-template-columns:1fr}.shell-nav{position:relative}.dashboard,.toolbar,.explorer-shell,.topbar,.action-filter-bar,.content-inspector,.refresh-controls,.file-explorer-grid{grid-template-columns:1fr}main{padding:14px}dl{grid-template-columns:1fr}.shell-nav nav{grid-template-columns:repeat(2,minmax(0,1fr))}.secondary-inspector{position:relative;top:auto;order:2}.topbar h1{font-size:24px}.panel[data-route-panel=login]{margin-top:10px;padding:18px}}"#
152}
153
154#[cfg(test)]
155mod tests {
156    use super::*;
157
158    #[test]
159    fn design_system_covers_first_boundary_token_features() {
160        let design = browser_ui_design_system();
161        let feature_ids: Vec<&str> = design
162            .tokens
163            .iter()
164            .map(|token| token.feature_id.as_str())
165            .collect();
166
167        assert_eq!(design.implementation_status, "implemented");
168        assert!(feature_ids.contains(&"feat:bucketwarden.ui.browser.tokens.color"));
169        assert!(feature_ids.contains(&"feat:bucketwarden.ui.browser.tokens.spacing"));
170        assert!(feature_ids.contains(&"feat:bucketwarden.ui.browser.tokens.typography"));
171        assert!(feature_ids.contains(&"feat:bucketwarden.ui.browser.tokens.table"));
172        assert!(feature_ids.contains(&"feat:bucketwarden.ui.browser.tokens.focus-status"));
173        assert!(design
174            .component_contracts
175            .iter()
176            .any(|component| component == "tables"));
177        assert!(design
178            .component_contracts
179            .iter()
180            .any(|component| component == "modals"));
181    }
182
183    #[test]
184    fn css_exposes_accessibility_and_responsive_contracts() {
185        let css = browser_ui_css();
186
187        assert!(css.contains(":focus"));
188        assert!(css.contains("[hidden]{display:none!important}"));
189        assert!(css.contains("overflow-wrap:anywhere"));
190        assert!(css.contains("@media(max-width:760px)"));
191        assert!(css.contains("--focus-ring"));
192        assert!(css.contains("body[data-route-chrome=login] .shell-nav{display:none}"));
193        assert!(css.contains("body[data-inspector-visible=false] .content-inspector"));
194        assert!(css.contains(".kpi-grid"));
195        assert!(css.contains(".gate-note"));
196        assert!(css.contains(".breadcrumb-item[aria-current=page]"));
197    }
198}