diff options
| author | Adam <[email protected]> | 2025-10-14 12:06:13 -0500 |
|---|---|---|
| committer | Adam <[email protected]> | 2025-10-14 12:06:18 -0500 |
| commit | 49e859cfd6565c4ef67388c232e639917ec20625 (patch) | |
| tree | 168101fe951797572cf6d723b9efe9474f073ed5 | |
| parent | 6c57a69af44d1a4844f92311e11242206c155212 (diff) | |
| download | opencode-49e859cfd6565c4ef67388c232e639917ec20625.tar.gz opencode-49e859cfd6565c4ef67388c232e639917ec20625.zip | |
wip: css/ui work
| -rw-r--r-- | packages/css/src/components/select.css | 113 | ||||
| -rw-r--r-- | packages/css/src/components/tabs.css | 98 | ||||
| -rw-r--r-- | packages/css/src/index.css | 1 | ||||
| -rw-r--r-- | packages/ui/index.html | 2 | ||||
| -rw-r--r-- | packages/ui/src/app.tsx | 146 | ||||
| -rw-r--r-- | packages/ui/src/components/index.ts | 1 | ||||
| -rw-r--r-- | packages/ui/src/components/select.tsx | 10 | ||||
| -rw-r--r-- | packages/ui/src/components/tabs.tsx | 74 | ||||
| -rw-r--r-- | packages/ui/src/index.css | 3 | ||||
| -rw-r--r-- | packages/ui/src/index.tsx | 1 |
10 files changed, 325 insertions, 124 deletions
diff --git a/packages/css/src/components/select.css b/packages/css/src/components/select.css index d7b4b589d..2403bfb26 100644 --- a/packages/css/src/components/select.css +++ b/packages/css/src/components/select.css @@ -1,20 +1,4 @@ [data-component="select"] { - display: inline-flex; - align-items: center; - gap: calc(var(--spacing) * 2); - border-style: solid; - border-width: 1px; - border-radius: var(--radius-md); - border-color: var(--color-smoke-4); - font-family: var(--font-sans); - font-size: var(--text-base); - line-height: var(--text-base--line-height); - font-weight: var(--font-weight-normal); - cursor: pointer; - transition: all 0.2s ease-in-out; - text-decoration: none; - user-select: none; - &:disabled { opacity: 0.5; cursor: not-allowed; @@ -28,38 +12,35 @@ [data-slot="section"] { font-size: var(--text-xs); line-height: var(--text-xs--line-height); - font-weight: var(--font-weight-normal); + font-weight: var(--font-weight-light); + text-transform: uppercase; + color: var(--text-default-text-weak); + opacity: 0.6; margin-top: calc(var(--spacing) * 3); margin-left: calc(var(--spacing) * 2); &:first-child { - margin-top: calc(var(--spacing) * 0); + margin-top: 0; } } [data-slot="item"] { - /* "relative flex cursor-pointer select-none items-center": true, */ - /* "rounded-sm px-2 py-0.5 text-xs outline-none text-text": true, */ - /* "transition-colors data-[disabled]:pointer-events-none": true, */ - /* "data-[highlighted]:bg-background-element data-[disabled]:opacity-50": true, */ position: relative; display: flex; align-items: center; - justify-content: center; - gap: calc(var(--spacing) * 2); - border-style: solid; - border-width: 1px; - border-radius: var(--radius-md); - font-family: var(--font-sans); - font-size: var(--text-base); - line-height: var(--text-base--line-height); - font-weight: var(--font-weight-normal); + padding: calc(var(--spacing) * 2) calc(var(--spacing) * 2); + border-radius: var(--radius-sm); + font-size: var(--text-xs); + line-height: var(--text-xs--line-height); + color: var(--text-default-text); cursor: pointer; - transition: all 0.2s ease-in-out; - text-decoration: none; + transition: + background-color 0.2s ease-in-out, + color 0.2s ease-in-out; + outline: none; user-select: none; &[data-highlighted] { - background-color: var(--color-background-element); + background-color: var(--surface-default-surface); } &[data-disabled] { @@ -67,16 +48,7 @@ opacity: 0.5; } - /* [data-slot="item-label"] { */ - /* font-size: var(--text-xs); */ - /* line-height: var(--text-xs--line-height); */ - /* font-weight: var(--font-weight-normal); */ - /* } */ - [data-slot="item-indicator"] { - /* display: flex; */ - /* align-items: center; */ - /* gap: calc(var(--spacing) * 1); */ margin-left: auto; } } @@ -88,46 +60,61 @@ white-space: nowrap; } [data-slot="icon"] { - /* "group size-fit shrink-0 text-text-muted transition-transform duration-100": true, */ width: fit-content; height: fit-content; flex-shrink: 0; + color: var(--text-default-text-weak); + transition: transform 0.1s ease-in-out; } } } [data-component="select-content"] { - /* "min-w-32 overflow-hidden rounded-md border border-border-subtle/40": true, */ - /* "bg-background-panel p-1 shadow-md z-50": true, */ - /* "data-[closed]:animate-out data-[closed]:fade-out-0 data-[closed]:zoom-out-95": true, */ - /* "data-[expanded]:animate-in data-[expanded]:fade-in-0 data-[expanded]:zoom-in-95": true, */ min-width: 8rem; overflow: hidden; border-radius: var(--radius-md); border-width: 1px; border-style: solid; - border-color: var(--color-smoke-4); - background-color: var(--color-smoke-2); + border-color: var(--border-default-border-weak); + background-color: var(--surface-raised-surface-raised); padding: calc(var(--spacing) * 1); + box-shadow: var(--shadow-md); z-index: 50; - /* &[data-closed] { */ - /* animation: fade-out-0 0.2s ease-out; */ - /* animation-fill-mode: forwards; */ - /* animation-delay: 0.2s; */ - /* opacity: 0; */ - /* } */ - /* &[data-expanded] { */ - /* animation: fade-in-0 0.2s ease-out; */ - /* animation-fill-mode: forwards; */ - /* animation-delay: 0.2s; */ - /* opacity: 1; */ - /* } */ + &[data-closed] { + animation: select-close 0.15s ease-out; + } + + &[data-expanded] { + animation: select-open 0.15s ease-out; + } + [data-slot="list"] { - /* overflow-y-auto max-h-48 whitespace-nowrap overflow-x-hidden */ overflow-y: auto; max-height: 12rem; white-space: nowrap; overflow-x: hidden; } } + +@keyframes select-open { + from { + opacity: 0; + transform: scale(0.95); + } + to { + opacity: 1; + transform: scale(1); + } +} + +@keyframes select-close { + from { + opacity: 1; + transform: scale(1); + } + to { + opacity: 0; + transform: scale(0.95); + } +} diff --git a/packages/css/src/components/tabs.css b/packages/css/src/components/tabs.css new file mode 100644 index 000000000..3a70b4867 --- /dev/null +++ b/packages/css/src/components/tabs.css @@ -0,0 +1,98 @@ +[data-component="tabs"] { + display: flex; + flex-direction: column; + height: 100%; + + & [data-slot="list"] { + position: relative; + display: flex; + align-items: center; + background-color: var(--surface-default-surface); + overflow-x: auto; + + /* Hide scrollbar */ + scrollbar-width: none; + -ms-overflow-style: none; + + &::-webkit-scrollbar { + display: none; + } + + /* Divider between tabs */ + & > [data-slot="trigger"]:not(:first-child) { + border-left: 1px solid var(--border-default-border-weak); + } + + /* After element to fill remaining space */ + &::after { + content: ""; + display: block; + flex-grow: 1; + height: calc(var(--spacing) * 8); + border-left: 1px solid var(--border-default-border-weak); + border-bottom: 1px solid var(--border-default-border-weak); + } + + &:empty::after { + border-left: none; + } + } + + & [data-slot="trigger"] { + position: relative; + padding: 0 calc(var(--spacing) * 3); + height: calc(var(--spacing) * 8); + display: flex; + align-items: center; + font-size: var(--text-sm); + font-weight: var(--font-weight-medium); + color: var(--text-default-text-weak); + cursor: pointer; + white-space: nowrap; + flex-shrink: 0; + border-bottom: 1px solid var(--border-default-border-weak); + background-color: transparent; + transition: + background-color 0.15s ease, + color 0.15s ease; + + &:disabled { + pointer-events: none; + opacity: 0.5; + } + + &:focus-visible { + outline: none; + box-shadow: 0 0 0 2px var(--border-default-border-focus); + } + + &[data-selected] { + color: var(--text-default-text); + background-color: var(--surface-panel-surface); + border-bottom-color: transparent; + } + + &:hover:not(:disabled):not([data-selected]) { + color: var(--text-default-text); + } + } + + & [data-slot="content"] { + background-color: var(--surface-panel-surface); + overflow-y: auto; + flex: 1; + + /* Hide scrollbar */ + scrollbar-width: none; + -ms-overflow-style: none; + + &::-webkit-scrollbar { + display: none; + } + + &:focus-visible { + outline: none; + box-shadow: 0 0 0 2px var(--border-default-border-focus); + } + } +} diff --git a/packages/css/src/index.css b/packages/css/src/index.css index 731fc6f3c..1ac357fe1 100644 --- a/packages/css/src/index.css +++ b/packages/css/src/index.css @@ -8,5 +8,6 @@ @import "./components/button.css" layer(components); @import "./components/icon.css" layer(components); @import "./components/select.css" layer(components); +@import "./components/tabs.css" layer(components); @import "./utilities.css" layer(utilities); diff --git a/packages/ui/index.html b/packages/ui/index.html index b7f74e79d..7697a5f96 100644 --- a/packages/ui/index.html +++ b/packages/ui/index.html @@ -1,5 +1,5 @@ <!doctype html> -<html lang="en" class="light"> +<html lang="en"> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> diff --git a/packages/ui/src/app.tsx b/packages/ui/src/app.tsx index be2ec357e..f42781543 100644 --- a/packages/ui/src/app.tsx +++ b/packages/ui/src/app.tsx @@ -1,66 +1,100 @@ import type { Component } from "solid-js" -import { Button } from "./components/button" -import { Select } from "./components" +import { Button, Select, Tabs } from "./components" import "@opencode-ai/css" import "./index.css" const App: Component = () => { + const Content = (props: { dark?: boolean }) => ( + <div class={`${props.dark ? "dark" : ""}`}> + <h3>Buttons</h3> + <section> + <Button variant="primary" size="normal"> + Normal Primary + </Button> + <Button variant="secondary" size="normal"> + Normal Secondary + </Button> + <Button variant="ghost" size="normal"> + Normal Ghost + </Button> + <Button variant="primary" size="large"> + Large Primary + </Button> + <Button variant="secondary" size="large"> + Large Secondary + </Button> + <Button variant="ghost" size="large"> + Large Ghost + </Button> + </section> + <h3>Select</h3> + <section> + <Select + // we have to pass dark bc of the portal, + // normally wouldn't be needed bc root element + // would have theme class + class={props.dark ? "dark" : ""} + variant="primary" + options={["Option 1", "Option 2", "Option 3"]} + placeholder="Select Primary" + /> + <Select + variant="secondary" + class={props.dark ? "dark" : ""} + options={["Option 1", "Option 2", "Option 3"]} + placeholder="Select Secondary" + /> + <Select + variant="ghost" + class={props.dark ? "dark" : ""} + options={["Option 1", "Option 2", "Option 3"]} + placeholder="Select Ghost" + /> + </section> + <h3>Tabs</h3> + <section> + <Tabs defaultValue="tab1" style={{ width: "100%" }}> + <Tabs.List> + <Tabs.Trigger value="tab1">Tab 1</Tabs.Trigger> + <Tabs.Trigger value="tab2">Tab 2</Tabs.Trigger> + <Tabs.Trigger value="tab3">Tab 3</Tabs.Trigger> + <Tabs.Trigger value="tab4" disabled> + Disabled Tab + </Tabs.Trigger> + </Tabs.List> + <Tabs.Content value="tab1"> + <div style={{ padding: "16px" }}> + <h4>Tab 1 Content</h4> + <p>This is the content for the first tab.</p> + </div> + </Tabs.Content> + <Tabs.Content value="tab2"> + <div style={{ padding: "16px" }}> + <h4>Tab 2 Content</h4> + <p>This is the content for the second tab.</p> + </div> + </Tabs.Content> + <Tabs.Content value="tab3"> + <div style={{ padding: "16px" }}> + <h4>Tab 3 Content</h4> + <p>This is the content for the third tab.</p> + </div> + </Tabs.Content> + <Tabs.Content value="tab4"> + <div style={{ padding: "16px" }}> + <h4>Tab 4 Content</h4> + <p>This tab should be disabled.</p> + </div> + </Tabs.Content> + </Tabs> + </section> + </div> + ) + return ( <main> - <div class="light"> - <h3>Buttons</h3> - <section> - <Button variant="primary" size="normal"> - Normal Primary - </Button> - <Button variant="secondary" size="normal"> - Normal Secondary - </Button> - <Button variant="ghost" size="normal"> - Normal Ghost - </Button> - <Button variant="primary" size="large"> - Large Primary - </Button> - <Button variant="secondary" size="large"> - Large Secondary - </Button> - <Button variant="ghost" size="large"> - Large Ghost - </Button> - </section> - <h3>Select</h3> - <section> - <Select options={["a", "b", "c"]} onSelect={(x) => console.log(x)} placeholder="Select" /> - </section> - </div> - <div class="dark"> - <h3>Buttons</h3> - <section> - <Button variant="primary" size="normal"> - Normal Primary - </Button> - <Button variant="secondary" size="normal"> - Normal Secondary - </Button> - <Button variant="ghost" size="normal"> - Normal Ghost - </Button> - <Button variant="primary" size="large"> - Large Primary - </Button> - <Button variant="secondary" size="large"> - Large Secondary - </Button> - <Button variant="ghost" size="large"> - Large Ghost - </Button> - </section> - <h3>Select</h3> - <section> - <Select options={["a", "b", "c"]} onSelect={(x) => console.log(x)} placeholder="Select" /> - </section> - </div> + <Content /> + <Content dark /> </main> ) } diff --git a/packages/ui/src/components/index.ts b/packages/ui/src/components/index.ts index 80d80299b..be12d6a67 100644 --- a/packages/ui/src/components/index.ts +++ b/packages/ui/src/components/index.ts @@ -1,3 +1,4 @@ export * from "./button" export * from "./icon" export * from "./select" +export * from "./tabs" diff --git a/packages/ui/src/components/select.tsx b/packages/ui/src/components/select.tsx index c4d5443c1..ecf05d5e2 100644 --- a/packages/ui/src/components/select.tsx +++ b/packages/ui/src/components/select.tsx @@ -79,11 +79,17 @@ export function Select<T>(props: SelectProps<T> & ButtonProps) { }} </Kobalte.Value> <Kobalte.Icon data-slot="icon"> - <Icon name="chevron-down" size={16} class="-my-2 group-data-[expanded]:rotate-180" /> + <Icon name="chevron-down" size={16} /> </Kobalte.Icon> </Kobalte.Trigger> <Kobalte.Portal> - <Kobalte.Content data-component="select-content"> + <Kobalte.Content + classList={{ + ...(props.classList ?? {}), + [props.class ?? ""]: !!props.class, + }} + data-component="select-content" + > <Kobalte.Listbox data-slot="list" /> </Kobalte.Content> </Kobalte.Portal> diff --git a/packages/ui/src/components/tabs.tsx b/packages/ui/src/components/tabs.tsx new file mode 100644 index 000000000..5e047a7ca --- /dev/null +++ b/packages/ui/src/components/tabs.tsx @@ -0,0 +1,74 @@ +import { Tabs as Kobalte } from "@kobalte/core/tabs" +import { splitProps } from "solid-js" +import type { ComponentProps, ParentProps } from "solid-js" + +export interface TabsProps extends ComponentProps<typeof Kobalte> {} +export interface TabsListProps extends ComponentProps<typeof Kobalte.List> {} +export interface TabsTriggerProps extends ComponentProps<typeof Kobalte.Trigger> {} +export interface TabsContentProps extends ComponentProps<typeof Kobalte.Content> {} + +function TabsRoot(props: TabsProps) { + const [split, rest] = splitProps(props, ["class", "classList"]) + return ( + <Kobalte + {...rest} + data-component="tabs" + classList={{ + ...(split.classList ?? {}), + [split.class ?? ""]: !!split.class, + }} + /> + ) +} + +function TabsList(props: TabsListProps) { + const [split, rest] = splitProps(props, ["class", "classList"]) + return ( + <Kobalte.List + {...rest} + data-slot="list" + classList={{ + ...(split.classList ?? {}), + [split.class ?? ""]: !!split.class, + }} + /> + ) +} + +function TabsTrigger(props: ParentProps<TabsTriggerProps>) { + const [split, rest] = splitProps(props, ["class", "classList", "children"]) + return ( + <Kobalte.Trigger + {...rest} + data-slot="trigger" + classList={{ + ...(split.classList ?? {}), + [split.class ?? ""]: !!split.class, + }} + > + {split.children} + </Kobalte.Trigger> + ) +} + +function TabsContent(props: ParentProps<TabsContentProps>) { + const [split, rest] = splitProps(props, ["class", "classList", "children"]) + return ( + <Kobalte.Content + {...rest} + data-slot="content" + classList={{ + ...(split.classList ?? {}), + [split.class ?? ""]: !!split.class, + }} + > + {split.children} + </Kobalte.Content> + ) +} + +export const Tabs = Object.assign(TabsRoot, { + List: TabsList, + Trigger: TabsTrigger, + Content: TabsContent, +}) diff --git a/packages/ui/src/index.css b/packages/ui/src/index.css index be7adaaed..b5f1cf302 100644 --- a/packages/ui/src/index.css +++ b/packages/ui/src/index.css @@ -12,7 +12,7 @@ } main > div { flex: 1; - padding: 3rem; + padding: 2rem; min-width: 0; overflow-y: auto; overflow-x: hidden; @@ -24,6 +24,7 @@ font-size: 1.25rem; font-weight: 600; margin: 0 0 1rem 0; + margin-bottom: -1rem; } section { display: flex; diff --git a/packages/ui/src/index.tsx b/packages/ui/src/index.tsx index 216765711..0e4c4a018 100644 --- a/packages/ui/src/index.tsx +++ b/packages/ui/src/index.tsx @@ -1,6 +1,5 @@ /* @refresh reload */ import { render } from "solid-js/web" -import "solid-devtools" import App from "./app" |
