🎉 Improve div structure

This commit is contained in:
Eva Marco 2026-07-27 14:21:31 +02:00
parent 1c21b1b1bd
commit cfc3f252c7
2 changed files with 274 additions and 366 deletions

View File

@ -20,6 +20,7 @@
[app.main.ui.components.file-uploader :refer [file-uploader]]
[app.main.ui.ds.buttons.button :refer [button*]]
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
[app.main.ui.ds.controls.checkbox :refer [checkbox*]]
[app.main.ui.ds.controls.select :refer [select*]]
[app.main.ui.ds.foundations.assets.icon :as i :refer [icon*]]
[app.main.ui.ds.foundations.assets.raw-svg :as rsvg :refer [raw-svg*]]
@ -307,7 +308,7 @@
:typography t/body-medium}
(:name entry)
(when ^boolean is-shared?
[:> icon* {:icon-id i/library :class (stl/css :icon)}])]
[:> icon* {:icon-id i/library :class (stl/css :file-label-icon)}])]
[:> context-notification*
{:level level
:appearance :ghost
@ -317,7 +318,7 @@
:typography t/body-medium}
(:name entry)
(when ^boolean is-shared?
[:> icon* {:icon-id i/library :class (stl/css :icon)}])]])])
[:> icon* {:icon-id i/library :class (stl/css :file-label-icon)}])]])])
(when ^boolean (or editable? can-be-deleted)
[:div {:class (stl/css :edit-entry-buttons)}
@ -392,7 +393,15 @@
(mf/defc library-resolution*
{::mf/private true}
[{:keys [unresolved-file selection on-select]}]
(let [candidates (:pending unresolved-file)]
(let [candidates (:pending unresolved-file)
disconnected* (mf/use-state #{})
disconnected (deref disconnected*)
on-change-disconnected
(mf/use-fn
(fn [id]
(swap! disconnected*
(fn [s]
(if (contains? s id) (disj s id) (conj s id))))))]
;; Pre-select first candidate for each library
(mf/with-effect [candidates]
@ -407,21 +416,53 @@
:typography t/body-large}
"Some libraries couldn't be linked automatically. Select the correct library for each:"]
(for [{:keys [id name candidates]} candidates]
(let [options (mapv (fn [c]
{:id (str (:id c))
:label (str (:name c) " (" (:project-name c) ")")})
candidates)
selected (get selection id)]
[:div {:class (stl/css :library-resolution-item)
:key (dm/str id)}
[:div {:class (stl/css :library-resolution-item-name)}
name]
[:> select* {:options options
:class (stl/css :library-resolution-select)
:default-selected (or (some-> selected str) "")
:has-portal true
:on-change (partial on-select id)}]]))]))
[:table {:class (stl/css :library-resolution-table)}
[:thead
[:tr {:class (stl/css :library-resolution-header)}
[:th {:class (stl/css :library-origin-name)}
[:> icon* {:icon-id i/library
:class (stl/css :library-resolution-icon)
:size "s"}]
"original library"]
[:th {:class (stl/css :library-resolution-arrow)}]
[:th {:class (stl/css :library-resolution-connection)}
[:> icon* {:icon-id i/library
:class (stl/css :library-resolution-icon)
:size "s"}]
"connect to"]]]
[:tbody {:class (stl/css :library-resolution-body)}
(for [{:keys [id name candidates]} candidates]
(let [options (mapv (fn [c]
{:id (str (:id c))
:label (str (:name c) " (" (:project-name c) ")")})
candidates)
selected (get selection id)
is-conected (not (contains? disconnected id))]
[:tr {:class (stl/css :library-resolution-item)
:key (dm/str id)}
[:td {:class (stl/css :library-resolution-item-name)}
[:> checkbox* {:id (dm/str id)
:label name
:checked is-conected
:on-change #(on-change-disconnected id)}]]
[:td {:class (stl/css :library-resolution-arrow)}
[:> icon* {:icon-id i/row
:size "m"}]]
[:td
(if is-conected
[:> select* {:options options
:class (stl/css :library-resolution-select)
:default-selected (or (some-> selected str) "")
:has-portal true
:on-change (partial on-select id)}]
[:> text* {:class (stl/css :library-resolution-no-selection)
:as "span"
:typography t/body-small}
(let [selected-c (or (some #(when (= (str (:id %)) selected) %) candidates)
(first candidates))]
(dm/str (:name selected-c) " (" (:project-name selected-c) ")"))])]]))]]]))
(mf/defc library-resolution-summary-file*
{::mf/private true}
@ -430,19 +471,16 @@
pending (:pending resolution-file)]
[:div {:class (stl/css :summary-file)}
[:div {:class (stl/css :summary-file-header)}
[:> icon* {:icon-id i/library
[:> icon* {:icon-id i/document
:class (stl/css :summary-file-icon)
:size "s"}]
[:span {:class (stl/css :summary-file-name)}
[:> text* {:class (stl/css :summary-file-name)
:as "span"
:typography t/body-medium}
(:name resolution-file)]]
(when (seq done)
[:div {:class (stl/css :summary-section)}
[:div {:class (stl/css :summary-section-header)}
[:> icon* {:icon-id i/status-tick
:class (stl/css :summary-section-icon)
:size "s"}]
[:span (tr "dashboard.import.summary.auto-linked")]]
[:ul {:class (stl/css :summary-list)}
(for [{:keys [name]} done]
[:li {:class (stl/css :summary-list-item)
@ -457,11 +495,17 @@
(when (seq pending)
[:div {:class (stl/css :summary-section)}
[:div {:class (stl/css :summary-section-header)}
[:> icon* {:icon-id i/open-link
:class (stl/css :summary-section-icon)
:size "s"}]
[:span (tr "dashboard.import.summary.your-selection")]]
[:> text* {:as "span"
:typography t/headline-small}
"linked manually"]]
[:ul {:class (stl/css :summary-list)}
[:li {:class (stl/css :summary-list-item)
:key "summary-list-header"}
[:span {:class (stl/css :summary-item-name-header)}
"Original"]
[:span {:class (stl/css :summary-item-name-header)}
"New"]]
(for [{:keys [id name] :as cand} pending]
(let [selected-id (get selection id)
selected-c (when selected-id
@ -469,6 +513,9 @@
[:li {:class (stl/css :summary-list-item)
:key (dm/str id)}
[:span {:class (stl/css :summary-item-name)} name]
[:> icon* {:icon-id i/row
:size "m"
:class (stl/css :summary-linked-arrow)}]
(if selected-c
[:span {:class (stl/css :summary-linked-info)}
[:span {:class (stl/css :summary-linked-name)}
@ -835,50 +882,60 @@
(= :library-resolution status)
[:*
(when (seq visited)
[:input {:class (stl/css :cancel-button)
:type "button"
:value (tr "labels.previous")
:on-click on-wizard-prev}])
[:> button* {:class (stl/css :cancel-button)
:variant "secondary"
:on-click on-wizard-prev}
(tr "labels.previous")])
;; Label flips to "Review" when this is the last unvisited unresolved file.
[:input {:class (stl/css :accept-btn)
:type "button"
:value (if all-visited?
(tr "labels.next")
(tr "dashboard.import.review-links"))
:on-click on-wizard-next}]]
[:> button* {:class (stl/css :accept-btn)
:variant "primary"
:on-click on-wizard-next}
(if all-visited?
(tr "labels.next")
(tr "dashboard.import.review-links"))]]
;; Summary: Confirm / Back
;; Back pops the stack once and re-enters the wizard at the popped file.
(= :library-summary status)
[:*
(when (seq visited)
[:input {:class (stl/css :cancel-button)
:type "button"
:value (tr "labels.back")
:on-click on-summary-back}])
[:input {:class (stl/css :accept-btn)
:type "button"
:value (tr "dashboard.import.confirm-library-links")
:on-click on-confirm-library-links}]]
[:> button* {:class (stl/css :cancel-button)
:variant "secondary"
:on-click on-summary-back}
(tr "labels.back")])
[:> button* {:class (stl/css :accept-btn)
:variant "primary"
:on-click on-confirm-library-links}
(tr "dashboard.import.confirm-library-links")]]
(= :analyze status)
[:input {:class (stl/css :cancel-button)
:type "button"
:value (tr "labels.cancel")
:on-click on-cancel}]
[:> button* {:class (stl/css :cancel-button)
:variant "secondary"
:on-click on-cancel}
(tr "labels.cancel")]
(= status :import-error)
[:> button* {:class (stl/css :accept-btn)
:variant "primary"
:on-click on-accept}
(tr "labels.accept")]
(= status :import-success)
[:> button* {:class (stl/css :accept-btn)
:variant "primary"
:on-click on-accept}
(tr "labels.accept")]
(= status :import-ready)
[:input {:class (stl/css :accept-btn)
:type "button"
:value (tr "labels.continue")
:disabled pending-analysis?
:on-click on-continue}]
[:> button* {:class (stl/css :accept-btn)
:variant "primary"
:on-click on-continue}
(tr "labels.continue")]
(or (= :import-success status)
(= :import-error status)
(= :import-progress status))
[:input {:class (stl/css :accept-btn)
:type "button"
:value (tr "labels.accept")
:disabled (= :import-progress status)
:on-click on-accept}])]]]]))
[:> button* {:class (stl/css :accept-btn)
:variant "primary"
:on-click on-accept}
(tr "labels.accept")])]]]]))

View File

@ -35,66 +35,10 @@
border: $b-2 solid var(--color-background-quaternary);
min-block-size: $sz-192;
inline-size: $sz-512;
max-block-size: calc(10 * px2rem(80));
max-block-size: px2rem(800);
}
.modal-content {
display: flex;
flex-direction: column;
gap: var(--sp-l);
margin-block-end: var(--sp-xxl);
}
.modal-title {
color: var(--color-foreground-primary);
}
.modal-close-btn {
// %button-tertiary essentials
border-radius: $br-8;
background-color: transparent;
border: $b-2 solid transparent;
color: var(--button-tertiary-foreground-color-rest);
cursor: pointer;
display: flex;
justify-content: center;
align-items: center;
position: absolute;
inset-block-start: var(--sp-s);
inset-inline-end: px2rem(6);
block-size: $sz-32;
inline-size: $sz-28;
svg {
display: flex;
justify-content: center;
align-items: center;
block-size: $sz-16;
inline-size: $sz-16;
color: transparent;
fill: none;
stroke-width: 1px;
stroke: var(--button-tertiary-foreground-color-rest);
}
&:hover {
background-color: var(--button-tertiary-background-color-hover);
color: var(--button-tertiary-foreground-color-hover);
border-color: var(--button-secondary-border-color-hover);
svg {
stroke: var(--button-tertiary-foreground-color-hover);
}
}
}
.modal-content {
// TODO: Typography does not match any existing DS typography token.
// body-small-typography: line-height 1.4 (DS body-small has line-height 1.3)
font-family: "worksans", "vazirmatn", sans-serif;
font-size: px2rem(12);
font-weight: 400;
line-height: 1.4;
flex: 1;
overflow: hidden auto;
display: grid;
@ -104,13 +48,19 @@
min-block-size: px2rem(40);
}
.modal-title {
color: var(--color-foreground-primary);
}
.modal-close-btn {
position: absolute;
inset-block-start: var(--sp-s);
inset-inline-end: px2rem(6);
}
.status-message {
// TODO: Typography does not match any existing DS typography token.
// body-small-typography: line-height 1.4 (DS body-small has line-height 1.3)
font-family: "worksans", "vazirmatn", sans-serif;
font-size: px2rem(12);
font-weight: 400;
line-height: 1.4;
@include use-typography("body-small");
color: var(--color-foreground-primary);
font-style: italic;
}
@ -121,105 +71,6 @@
gap: var(--sp-l);
}
.cancel-button {
// %modal-cancel-btn essentials (these are <input type=button> elements)
border-radius: $br-8;
background-color: var(--button-secondary-background-color-rest);
border: $b-1 solid var(--button-secondary-border-color-rest);
color: var(--button-secondary-foreground-color-rest);
display: flex;
justify-content: center;
align-items: center;
padding: var(--sp-s) var(--sp-xxl);
block-size: $sz-32;
margin: 0;
cursor: pointer;
// TODO: Typography does not match any existing DS typography token.
// uppercase-title-typography: 11px, weight 500, line-height 1.2, uppercase
font-family: "worksans", "vazirmatn", sans-serif;
font-size: px2rem(11);
font-weight: 500;
line-height: 1.2;
text-transform: uppercase;
&:hover {
background-color: var(--button-secondary-background-color-hover);
border-color: var(--button-secondary-border-color-hover);
color: var(--button-secondary-foreground-color-hover);
}
&:active {
background-color: var(--button-secondary-background-color-active);
border-color: var(--button-secondary-border-color-active);
color: var(--button-secondary-foreground-color-active);
}
&[disabled],
&:disabled {
background-color: var(--button-background-color-disabled);
border-color: var(--button-border-color-disabled);
color: var(--button-foreground-color-disabled);
cursor: unset;
}
}
.accept-btn {
// %modal-accept-btn essentials
border-radius: $br-8;
background-color: var(--button-primary-background-color-rest);
border: $b-1 solid var(--button-primary-border-color-rest);
color: var(--button-primary-foreground-color-rest);
display: flex;
justify-content: center;
align-items: center;
padding: var(--sp-s) var(--sp-xxl);
block-size: $sz-32;
margin: 0;
cursor: pointer;
// TODO: Typography does not match any existing DS typography token.
// uppercase-title-typography: 11px, weight 500, line-height 1.2, uppercase
font-family: "worksans", "vazirmatn", sans-serif;
font-size: px2rem(11);
font-weight: 500;
line-height: 1.2;
text-transform: uppercase;
&:hover {
background-color: var(--button-primary-background-color-hover);
border-color: var(--button-primary-border-color-hover);
color: var(--button-primary-foreground-color-hover);
}
&:active {
background-color: var(--button-primary-background-color-active);
border-color: var(--button-primary-border-color-active);
color: var(--button-primary-foreground-color-active);
}
&[disabled],
&:disabled {
background-color: var(--button-background-color-disabled);
border-color: var(--button-border-color-disabled);
color: var(--button-foreground-color-disabled);
cursor: unset;
}
&.danger {
background-color: var(--modal-button-background-color-error);
border-color: var(--modal-button-background-color-error);
color: var(--modal-button-foreground-color-error);
}
}
// TODO: Verify before removal. These selectors appear to be unused.
// .modal-scd-msg,
// .modal-subtitle,
// .modal-msg {
// ...
// }
.file-entry {
--file-entry-fg-color: var(--color-foreground-secondary);
@ -227,57 +78,6 @@
flex-direction: column;
gap: var(--sp-m);
.error-message,
.progress-message {
display: flex;
align-items: center;
min-block-size: $sz-32;
color: var(--file-entry-fg-color);
}
.error-message {
align-items: flex-start;
white-space: pre-wrap;
overflow-wrap: anywhere;
}
.linked-library {
display: flex;
align-items: center;
gap: var(--sp-m);
color: var(--file-entry-fg-color);
.linked-library-tag {
display: flex;
justify-content: center;
align-items: center;
block-size: $sz-24;
inline-size: $sz-24;
svg {
display: flex;
justify-content: center;
align-items: center;
block-size: $sz-16;
inline-size: $sz-16;
color: transparent;
fill: none;
stroke-width: 1px;
stroke: var(--file-entry-fg-color);
}
&.error {
svg {
stroke: var(--element-foreground-error);
}
}
}
}
&.warning {
--file-entry-fg-color: var(--color-accent-warning);
}
&.success {
--file-entry-fg-color: var(--color-accent-sucess);
}
@ -291,6 +91,53 @@
}
}
.error-message,
.progress-message {
display: flex;
align-items: center;
min-block-size: $sz-32;
color: var(--file-entry-fg-color);
}
.error-message {
align-items: flex-start;
white-space: pre-wrap;
overflow-wrap: anywhere;
}
.linked-library {
display: flex;
align-items: center;
gap: var(--sp-m);
color: var(--file-entry-fg-color);
}
.linked-library-tag {
display: flex;
justify-content: center;
align-items: center;
block-size: $sz-24;
inline-size: $sz-24;
svg {
display: flex;
justify-content: center;
align-items: center;
block-size: $sz-16;
inline-size: $sz-16;
color: transparent;
fill: none;
stroke-width: 1px;
stroke: var(--file-entry-fg-color);
}
&.error {
svg {
stroke: var(--element-foreground-error);
}
}
}
.file-name {
display: flex;
align-items: center;
@ -298,24 +145,11 @@
color: var(--file-entry-fg-color);
}
.edit-entry-buttons {
display: flex;
align-items: center;
gap: var(--sp-xs);
// button {
// display: flex;
// justify-content: center;
// align-items: center;
// border-radius: $br-8;
// background-color: transparent;
// border: $b-2 solid transparent;
// inline-size: $sz-28;
// block-size: $sz-32;
// cursor: pointer;
// color: var(--button-tertiary-foreground-color-rest);
// }
}
.edit-entry-buttons {
display: flex;
align-items: center;
gap: var(--sp-xs);
}
.file-name-edit {
display: flex;
@ -329,24 +163,21 @@
background-color: var(--color-background-tertiary);
}
.file-name-label {
display: flex;
align-items: center;
gap: var(--sp-m);
flex-grow: 1;
.icon {
display: flex;
justify-content: center;
align-items: center;
block-size: $sz-16;
inline-size: $sz-16;
color: var(--file-entry-fg-color);
}
}
.file-label-icon {
display: flex;
justify-content: center;
align-items: center;
block-size: $sz-16;
inline-size: $sz-16;
color: var(--file-entry-fg-color);
}
.file-name-input {
@include use-typography("body-medium");
@ -371,25 +202,6 @@
}
}
.file-icon,
.file-svg {
display: flex;
justify-content: center;
align-items: center;
block-size: $sz-24;
inline-size: $sz-16;
color: var(--file-entry-fg-color);
}
.file-svg {
fill: var(--file-entry-fg-color);
}
.context-notification-error {
--context-notification-bg-color: var(--color-background-primary);
}
.file-name-notification {
flex-grow: 1;
}
@ -419,6 +231,10 @@
overflow-wrap: anywhere;
}
// ################################
// LIBRARY RESOLUTION
// ################################
.library-resolution {
display: flex;
flex-direction: column;
@ -430,50 +246,96 @@
margin-block-end: var(--sp-s);
}
.library-resolution-header {
@include use-typography("body-medium");
color: var(--color-foreground-secondary);
display: grid;
grid-template-columns: 1fr 32px 1fr;
border-block-end: $b-1 solid var(--color-foreground-secondary);
}
.library-origin-name,
.library-resolution-connection {
block-size: $sz-32;
text-align: start;
color: var(--color-foreground-primary);
display: flex;
align-items: center;
}
.library-resolution-item {
display: grid;
grid-template-columns: 1fr 32px 1fr;
block-size: $sz-32;
margin-block: var(--sp-s);
}
.library-resolution-icon {
display: flex;
justify-content: center;
align-items: center;
margin-inline-end: var(--sp-s);
color: var(--color-foreground-secondary);
}
.library-resolution-arrow {
color: var(--color-foreground-secondary);
display: flex;
justify-content: center;
align-items: center;
min-block-size: $sz-32;
}
.library-resolution-body {
display: flex;
flex-direction: column;
gap: var(--sp-xs);
padding: var(--sp-s) 0;
gap: var(--sp-s);
}
.library-resolution-item-name {
// TODO: Typography does not match any existing DS typography token.
font-family: "worksans", "vazirmatn", sans-serif;
font-size: px2rem(12);
line-height: 1.4;
color: var(--color-foreground-primary);
font-weight: 700;
}
@include use-typography("body-medium");
.library-resolution-summary-file {
margin-block-end: var(--sp-l);
padding-block-end: var(--sp-m);
border-bottom: $b-1 solid var(--color-foreground-secondary);
}
.library-resolution-linked {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--sp-s);
color: var(--color-foreground-secondary);
font-size: px2rem(12);
padding-inline-start: var(--sp-s);
}
.library-resolution-project {
.library-resolution-no-selection {
display: flex;
align-items: center;
color: var(--color-foreground-secondary);
font-size: px2rem(12);
margin-block-end: var(--sp-s);
padding: var(--sp-s);
margin-inline-start: var(--sp-xxs);
block-size: $sz-32;
}
// Summary file card
// ################################
// Summary file card
// ################################
.summary-file {
display: flex;
flex-direction: column;
gap: var(--sp-s);
padding: var(--sp-m);
border: $b-1 solid var(--color-background-quaternary);
border-radius: $br-8;
background: var(--color-background-primary);
}
.summary-list {
display: flex;
flex-direction: column;
gap: var(--sp-xxs);
list-style: none;
padding: 0;
margin: 0;
border-inline-start: $b-1 solid var(--color-background-quaternary);
}
.summary-file-header {
display: flex;
align-items: center;
@ -488,10 +350,7 @@
}
.summary-file-name {
@include use-typography("body-medium");
color: var(--color-foreground-primary);
font-weight: 700;
}
// Section within a file (auto-linked or user selection)
@ -509,20 +368,6 @@
padding: var(--sp-xs) 0;
}
.summary-section-icon {
color: var(--color-foreground-secondary);
flex-shrink: 0;
}
.summary-list {
display: flex;
flex-direction: column;
gap: var(--sp-xxs);
list-style: none;
padding: 0;
margin: 0;
}
.summary-list-item {
display: flex;
align-items: center;
@ -537,11 +382,6 @@
}
.summary-item-name {
// TODO: Typography does not match any existing DS typography token.
font-family: "worksans", "vazirmatn", sans-serif;
font-size: px2rem(12);
font-weight: 400;
line-height: 1.4;
color: var(--color-foreground-primary);
flex: 1;
min-inline-size: 0;
@ -552,6 +392,17 @@
white-space: nowrap;
}
.summary-item-name-header {
color: var(--color-foreground-secondary);
flex: 1;
min-inline-size: 0;
display: block;
max-inline-size: 99%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
// Auto-linked badge
.summary-linked-badge {
display: inline-flex;