NobGit
public anord read

Ledger

Why work hard when you can work easier?

Languages

Repository composition by tracked source files.

TypeScript
TypeScript 86% CSS 10% SQL 3% Shell 1% HTML 0%
Create file Wiki Documentation
Clone
https://nobgit.com/orgs/anord/ledger.git
ssh://[email protected]:2222/orgs/anord/ledger.git

Commit

Continues UI Fixes

3d9ace8
Alex Nord <[email protected]> 3 months ago
apps/web/src/AppShell.tsx              | 24 +++++++++++++
 apps/web/src/components/PageEditor.tsx | 27 ++++++++++++---
 apps/web/src/docs-theme.css            | 63 ++++++++++++++++++++++++++++++++--
 3 files changed, 107 insertions(+), 7 deletions(-)

Diff

diff --git a/apps/web/src/AppShell.tsx b/apps/web/src/AppShell.tsx
index 49a8e9f..3164995 100644
--- a/apps/web/src/AppShell.tsx
+++ b/apps/web/src/AppShell.tsx
@@ -231,11 +231,14 @@ function DocsShell({
   children: React.ReactNode;
 }) {
   const location = useLocation();
+  const navigate = useNavigate();
   const [searchOpen, setSearchOpen] = useState(false);
   const [sidebarOpen, setSidebarOpen] = useState(false);
+  const [createOpen, setCreateOpen] = useState(false);
   const currentPageSlug = location.pathname.startsWith("/page/")
     ? decodeURIComponent(location.pathname.split("/page/")[1] ?? "")
     : undefined;
+  const canCreate = user?.role === "editor" || user?.role === "admin" || user?.role === "owner";
 
   useCommandShortcut(() => setSearchOpen(true));
 
@@ -261,6 +264,22 @@ function DocsShell({
         results={searchResults}
         isLoading={searchLoading}
       />
+      {createOpen ? (
+        <>
+          <div className="command-palette__overlay is-open" onClick={() => setCreateOpen(false)} />
+          <div className="editor-modal" role="dialog" aria-modal="true" aria-label="Create document">
+            <PageEditor
+              spaces={spaces}
+              variant="dialog"
+              onCancel={() => setCreateOpen(false)}
+              onCreated={(slug) => {
+                setCreateOpen(false);
+                navigate(`/page/${slug}`);
+              }}
+            />
+          </div>
+        </>
+      ) : null}
 
       {loadingNavigation ? (
         <SidebarSkeleton />
@@ -306,6 +325,11 @@ function DocsShell({
           <div className="app-header__right">
             {user ? (
               <>
+                {canCreate ? (
+                  <button type="button" className="button-primary" onClick={() => setCreateOpen(true)}>
+                    New doc
+                  </button>
+                ) : null}
                 <Link to="/admin/general" className="button-ghost">Admin</Link>
                 <button className="button-ghost" onClick={onLogout}>Sign out</button>
               </>
diff --git a/apps/web/src/components/PageEditor.tsx b/apps/web/src/components/PageEditor.tsx
index efeb408..89d49b9 100644
--- a/apps/web/src/components/PageEditor.tsx
+++ b/apps/web/src/components/PageEditor.tsx
@@ -12,12 +12,23 @@ const emptyPage = {
   tagNames: [] as string[]
 };
 
-export function PageEditor({ spaces }: { spaces: Array<{ id: string; name: string; key: string }> }) {
+export function PageEditor({
+  spaces,
+  variant = "panel",
+  onCreated,
+  onCancel
+}: {
+  spaces: Array<{ id: string; name: string; key: string }>;
+  variant?: "panel" | "dialog";
+  onCreated?: (slug: string) => void;
+  onCancel?: () => void;
+}) {
   const [form, setForm] = useState({
     ...emptyPage,
     spaceId: spaces[0]?.id ?? ""
   });
   const [status, setStatus] = useState<string | null>(null);
+  const isDialog = variant === "dialog";
 
   async function submit() {
     try {
@@ -31,17 +42,18 @@ export function PageEditor({ spaces }: { spaces: Array<{ id: string; name: strin
         ...emptyPage,
         spaceId: spaces[0]?.id ?? ""
       });
+      onCreated?.(result.slug);
     } catch (error) {
       setStatus(error instanceof Error ? error.message : "Could not save page");
     }
   }
 
   return (
-    <section className="panel">
+    <section className={isDialog ? "editor-dialog" : "panel"}>
       <div className="panel__header">
         <div>
           <p className="eyebrow">Publishing</p>
-          <h3>Create a new page</h3>
+          <h3>{isDialog ? "New document" : "Create a new page"}</h3>
         </div>
         <span className="pill">Editor</span>
       </div>
@@ -120,13 +132,18 @@ export function PageEditor({ spaces }: { spaces: Array<{ id: string; name: strin
       <label className="field">
         Markdown
         <textarea
-          className="editor-textarea"
+          className={`editor-textarea${isDialog ? " editor-textarea-dialog" : ""}`}
           value={form.bodyMarkdown}
           onChange={(event) => setForm((current) => ({ ...current, bodyMarkdown: event.target.value }))}
         />
       </label>
       <div className="panel__footer">
-        <button onClick={submit}>Publish draft</button>
+        <button onClick={submit}>Create draft</button>
+        {onCancel ? (
+          <button type="button" className="button-secondary" onClick={onCancel}>
+            Cancel
+          </button>
+        ) : null}
         {status ? <p className="muted">{status}</p> : null}
       </div>
     </section>
diff --git a/apps/web/src/docs-theme.css b/apps/web/src/docs-theme.css
index 64e0217..9c59639 100644
--- a/apps/web/src/docs-theme.css
+++ b/apps/web/src/docs-theme.css
@@ -127,6 +127,18 @@ a:focus {
   background: transparent;
 }
 
+.button-primary {
+  display: inline-flex;
+  align-items: center;
+  justify-content: center;
+  gap: 0.5rem;
+  border-radius: var(--radius-sm);
+  padding: 0.62rem 0.9rem;
+  background: var(--brand);
+  color: #111318;
+  font-weight: 600;
+}
+
 button:not(.button-ghost):not(.button-secondary):not(.tree-toggle):not(.space-link):not(.collection-card):not(.search-launcher):not(.home-tab) {
   border-radius: var(--radius-sm);
   padding: 0.78rem 1rem;
@@ -400,6 +412,31 @@ button:not(.button-ghost):not(.button-secondary):not(.tree-toggle):not(.space-li
   color: var(--text);
 }
 
+.editor-modal {
+  position: fixed;
+  top: 5vh;
+  left: 50%;
+  width: min(820px, calc(100% - 2rem));
+  max-height: 88vh;
+  transform: translateX(-50%);
+  z-index: 31;
+  overflow: auto;
+}
+
+.editor-dialog {
+  border: 1px solid var(--line);
+  background: var(--bg-elevated);
+  padding: 1.1rem 1.1rem 1rem;
+}
+
+.editor-textarea {
+  min-height: 320px;
+}
+
+.editor-textarea-dialog {
+  min-height: 420px;
+}
+
 .search-launcher kbd {
   border: 1px solid var(--line-strong);
   border-radius: 3px;
@@ -463,6 +500,12 @@ button:not(.button-ghost):not(.button-secondary):not(.tree-toggle):not(.space-li
   gap: 0.2rem;
   padding: 0.95rem 0;
   border-bottom: 1px solid var(--line);
+  transition: color 140ms ease, border-color 140ms ease;
+}
+
+.home-stream__item:hover {
+  color: #ffffff;
+  border-bottom-color: var(--line-strong);
 }
 
 .home-stream__title {
@@ -491,7 +534,7 @@ button:not(.button-ghost):not(.button-secondary):not(.tree-toggle):not(.space-li
 .setup-form {
   border: 1px solid var(--line);
   border-radius: var(--radius-lg);
-  background: transparent;
+  background: #10141a;
 }
 
 .overview-hero,
@@ -503,12 +546,18 @@ button:not(.button-ghost):not(.button-secondary):not(.tree-toggle):not(.space-li
   padding: 1.05rem 1.1rem;
 }
 
+.doc-card {
+  background: transparent;
+  border: 0;
+  padding: 0;
+}
+
 .overview-hero {
   display: grid;
   grid-template-columns: 1.2fr 0.9fr;
   gap: 1rem;
   align-items: end;
-  background: var(--bg-elevated);
+  background: transparent;
 }
 
 .overview-hero h1,
@@ -628,6 +677,7 @@ button:not(.button-ghost):not(.button-secondary):not(.tree-toggle):not(.space-li
   padding: 0.9rem 0 0.9rem 1rem;
   border: 0;
   border-left: 1px solid var(--line);
+  background: transparent;
 }
 
 .toc-rail a {
@@ -1103,6 +1153,15 @@ button:not(.button-ghost):not(.button-secondary):not(.tree-toggle):not(.space-li
   gap: 1rem;
 }
 
+.admin-content > .panel {
+  background: transparent;
+  border-left: 0;
+  border-right: 0;
+  border-radius: 0;
+  padding-left: 0;
+  padding-right: 0;
+}
+
 .settings-section {
   display: grid;
   gap: 0;