From 183e0911b76025a1f2a82e979d9834fec2131d0e Mon Sep 17 00:00:00 2001 From: Frank Date: Fri, 8 Aug 2025 13:22:54 -0400 Subject: wip: gateway --- cloud/web/src/ui/button.tsx | 24 + cloud/web/src/ui/context-dialog.tsx | 120 +++ cloud/web/src/ui/dialog-select.module.css | 36 + cloud/web/src/ui/dialog-select.tsx | 124 +++ cloud/web/src/ui/dialog-string.tsx | 70 ++ cloud/web/src/ui/dialog.tsx | 27 + cloud/web/src/ui/style/component/button.css | 78 ++ cloud/web/src/ui/style/component/dialog.css | 84 ++ cloud/web/src/ui/style/component/input.css | 34 + cloud/web/src/ui/style/component/label.css | 17 + cloud/web/src/ui/style/component/title-bar.css | 32 + cloud/web/src/ui/style/index.css | 50 + cloud/web/src/ui/style/token/animation.css | 23 + cloud/web/src/ui/style/token/color.css | 88 ++ cloud/web/src/ui/style/token/font.css | 20 + cloud/web/src/ui/style/token/reset.css | 212 ++++ cloud/web/src/ui/style/token/space.css | 38 + cloud/web/src/ui/svg/icons.tsx | 1292 ++++++++++++++++++++++++ cloud/web/src/ui/svg/index.tsx | 67 ++ 19 files changed, 2436 insertions(+) create mode 100644 cloud/web/src/ui/button.tsx create mode 100644 cloud/web/src/ui/context-dialog.tsx create mode 100644 cloud/web/src/ui/dialog-select.module.css create mode 100644 cloud/web/src/ui/dialog-select.tsx create mode 100644 cloud/web/src/ui/dialog-string.tsx create mode 100644 cloud/web/src/ui/dialog.tsx create mode 100644 cloud/web/src/ui/style/component/button.css create mode 100644 cloud/web/src/ui/style/component/dialog.css create mode 100644 cloud/web/src/ui/style/component/input.css create mode 100644 cloud/web/src/ui/style/component/label.css create mode 100644 cloud/web/src/ui/style/component/title-bar.css create mode 100644 cloud/web/src/ui/style/index.css create mode 100644 cloud/web/src/ui/style/token/animation.css create mode 100644 cloud/web/src/ui/style/token/color.css create mode 100644 cloud/web/src/ui/style/token/font.css create mode 100644 cloud/web/src/ui/style/token/reset.css create mode 100644 cloud/web/src/ui/style/token/space.css create mode 100644 cloud/web/src/ui/svg/icons.tsx create mode 100644 cloud/web/src/ui/svg/index.tsx (limited to 'cloud/web/src/ui') diff --git a/cloud/web/src/ui/button.tsx b/cloud/web/src/ui/button.tsx new file mode 100644 index 000000000..889102dda --- /dev/null +++ b/cloud/web/src/ui/button.tsx @@ -0,0 +1,24 @@ +import { Button as Kobalte } from "@kobalte/core/button" +import { JSX, Show, splitProps } from "solid-js" + +export interface ButtonProps { + color?: "primary" | "secondary" | "ghost" + size?: "md" | "sm" + icon?: JSX.Element +} +export function Button(props: JSX.IntrinsicElements["button"] & ButtonProps) { + const [split, rest] = splitProps(props, ["color", "size", "icon"]) + return ( + + +
{props.icon}
+
+ {props.children} +
+ ) +} diff --git a/cloud/web/src/ui/context-dialog.tsx b/cloud/web/src/ui/context-dialog.tsx new file mode 100644 index 000000000..f1bc93250 --- /dev/null +++ b/cloud/web/src/ui/context-dialog.tsx @@ -0,0 +1,120 @@ +import { createContext, JSX, ParentProps, useContext } from "solid-js" +import { StandardSchemaV1 } from "@standard-schema/spec" +import { createStore } from "solid-js/store" +import { Dialog } from "./dialog" + +const Context = createContext() + +type DialogControl = { + open>( + component: DialogComponent, + input: StandardSchemaV1.InferInput, + ): void + close(): void + isOpen(input: any): boolean + size: "sm" | "md" + transition?: boolean + input?: any +} + +type DialogProps> = { + input: StandardSchemaV1.InferInput + control: DialogControl +} + +type DialogComponent> = ReturnType< + typeof createDialog +> + +export function createDialog>(props: { + schema: Schema + size: "sm" | "md" + render: (props: DialogProps) => JSX.Element +}) { + const result = () => { + const dialog = useDialog() + return ( + { + if (!val) dialog.close() + }} + > + {props.render({ + input: dialog.input, + control: dialog, + })} + + ) + } + result.schema = props.schema + result.size = props.size + return result +} + +export function DialogProvider(props: ParentProps) { + const [store, setStore] = createStore<{ + dialog?: DialogComponent + input?: any + transition?: boolean + size: "sm" | "md" + }>({ + size: "sm", + }) + + const control: DialogControl = { + get input() { + return store.input + }, + get size() { + return store.size + }, + get transition() { + return store.transition + }, + isOpen(input) { + return store.dialog === input + }, + open(component, input) { + setStore({ + dialog: component, + input: input, + size: store.dialog !== undefined ? store.size : component.size, + transition: store.dialog !== undefined, + }) + + setTimeout(() => { + setStore({ + size: component.size, + }) + }, 0) + + setTimeout(() => { + setStore({ + transition: false, + }) + }, 150) + }, + close() { + setStore({ + dialog: undefined, + }) + }, + } + + return ( + <> + {props.children} + + ) +} + +export function useDialog() { + const ctx = useContext(Context) + if (!ctx) { + throw new Error("useDialog must be used within a DialogProvider") + } + return ctx +} diff --git a/cloud/web/src/ui/dialog-select.module.css b/cloud/web/src/ui/dialog-select.module.css new file mode 100644 index 000000000..4a99ef027 --- /dev/null +++ b/cloud/web/src/ui/dialog-select.module.css @@ -0,0 +1,36 @@ +.options { + margin-top: var(--space-1); + border-top: 2px solid var(--color-border); + padding: var(--space-2); + + [data-slot="option"] { + outline: none; + flex-shrink: 0; + height: var(--space-11); + display: flex; + justify-content: start; + align-items: center; + padding: 0 var(--space-2-5); + gap: var(--space-3); + cursor: pointer; + + &[data-empty] { + cursor: default; + color: var(--color-text-dimmed); + } + + &[data-active] { + background-color: var(--color-bg-surface); + } + + [data-slot="title"] { + font-size: var(--font-size-md); + } + + [data-slot="prefix"] { + width: var(--space-4); + height: var(--space-4); + } + } + +} diff --git a/cloud/web/src/ui/dialog-select.tsx b/cloud/web/src/ui/dialog-select.tsx new file mode 100644 index 000000000..087b94411 --- /dev/null +++ b/cloud/web/src/ui/dialog-select.tsx @@ -0,0 +1,124 @@ +import style from "./dialog-select.module.css" +import { z } from "zod" +import { createMemo, createSignal, For, JSX, onMount } from "solid-js" +import { createList } from "solid-list" +import { createDialog } from "./context-dialog" + +export const DialogSelect = createDialog({ + size: "md", + schema: z.object({ + title: z.string(), + placeholder: z.string(), + onSelect: z + .function(z.tuple([z.any()])) + .returns(z.void()) + .optional(), + options: z.array( + z.object({ + display: z.string(), + value: z.any().optional(), + onSelect: z.function().returns(z.void()).optional(), + prefix: z.custom().optional(), + }), + ), + }), + render: (ctx) => { + let input: HTMLInputElement + onMount(() => { + input.focus() + input.value = "" + }) + + const [filter, setFilter] = createSignal("") + const filtered = createMemo(() => + ctx.input.options?.filter((i) => + i.display.toLowerCase().includes(filter().toLowerCase()), + ), + ) + const list = createList({ + loop: true, + initialActive: 0, + items: () => filtered().map((_, i) => i), + handleTab: false, + }) + + const handleSelection = (index: number) => { + const option = ctx.input.options[index] + + // If the option has its own onSelect handler, use it + if (option.onSelect) { + option.onSelect() + } + // Otherwise, if there's a global onSelect handler, call it with the option's value + else if (ctx.input.onSelect) { + ctx.input.onSelect( + option.value !== undefined ? option.value : option.display, + ) + } + } + + return ( + <> +
+ +
+
+ { + setFilter(e.target.value) + list.setActive(0) + }} + onKeyDown={(e) => { + if (e.key === "Enter") { + const selected = list.active() + if (selected === null) return + handleSelection(selected) + return + } + if (e.key === "Escape") { + setFilter("") + return + } + list.onKeyDown(e) + }} + id={`dialog-select-${ctx.input.title}`} + ref={(r) => (input = r)} + data-slot="input" + placeholder={ctx.input.placeholder} + /> +
+
+ + No results +
+ } + > + {(option, index) => ( +
handleSelection(index())} + data-slot="option" + data-active={list.active() === index() ? true : undefined} + > + {option.prefix &&
{option.prefix}
} +
{option.display}
+
+ )} + + + + ) + }, +}) diff --git a/cloud/web/src/ui/dialog-string.tsx b/cloud/web/src/ui/dialog-string.tsx new file mode 100644 index 000000000..af2174786 --- /dev/null +++ b/cloud/web/src/ui/dialog-string.tsx @@ -0,0 +1,70 @@ +import { z } from "zod" +import { onMount } from "solid-js" +import { createDialog } from "./context-dialog" +import { Button } from "./button" + +export const DialogString = createDialog({ + size: "sm", + schema: z.object({ + title: z.string(), + placeholder: z.string(), + action: z.string(), + onSubmit: z.function().args(z.string()).returns(z.void()), + }), + render: (ctx) => { + let input: HTMLInputElement + onMount(() => { + setTimeout(() => { + input.focus() + input.value = "" + }, 50) + }) + + function submit() { + const value = input.value.trim() + if (value) { + ctx.input.onSubmit(value) + ctx.control.close() + } + } + + return ( + <> +
+ +
+
+ (input = r)} + placeholder={ctx.input.placeholder} + id={`dialog-string-${ctx.input.title}`} + onKeyDown={(e) => { + if (e.key === "Enter") { + e.preventDefault() + submit() + } + }} + /> +
+
+ + +
+ + ) + }, +}) diff --git a/cloud/web/src/ui/dialog.tsx b/cloud/web/src/ui/dialog.tsx new file mode 100644 index 000000000..101f23d2b --- /dev/null +++ b/cloud/web/src/ui/dialog.tsx @@ -0,0 +1,27 @@ +import { Dialog as Kobalte } from "@kobalte/core/dialog" +import { ComponentProps, ParentProps } from "solid-js" + +export type Props = ParentProps<{ + size?: "sm" | "md" + transition?: boolean +}> & + ComponentProps + +export function Dialog(props: Props) { + return ( + + + +
+ + {props.children} + +
+
+
+ ) +} diff --git a/cloud/web/src/ui/style/component/button.css b/cloud/web/src/ui/style/component/button.css new file mode 100644 index 000000000..9604f9865 --- /dev/null +++ b/cloud/web/src/ui/style/component/button.css @@ -0,0 +1,78 @@ +[data-component="button"] { + width: fit-content; + display: flex; + line-height: 1; + align-items: center; + justify-content: center; + gap: var(--space-2); + font-size: var(--font-size-md); + text-transform: uppercase; + height: var(--space-11); + outline: none; + font-weight: 500; + padding: 0 var(--space-4); + border-width: 2px; + border-color: var(--color-border); + cursor: pointer; + + &:disabled { + opacity: 0.5; + cursor: default; + } + + &[data-color="primary"] { + background-color: var(--color-text); + border-color: var(--color-text); + color: var(--color-text-invert); + + &:active { + border-color: var(--color-accent); + } + } + + &[data-color="secondary"] { + &:active { + border-color: var(--color-accent); + } + } + + &[data-color="ghost"] { + border: none; + text-decoration: underline; + + &:active { + color: var(--color-text-accent); + } + } + + &:has([data-slot="icon"]) { + padding-left: var(--space-3); + padding-right: var(--space-3); + } + + &[data-size="sm"] { + height: var(--space-8); + padding: var(--space-3); + font-size: var(--font-size-xs); + + [data-slot="icon"] { + width: var(--space-3-5); + height: var(--space-3-5); + } + + &:has([data-slot="icon"]) { + padding-left: var(--space-2); + padding-right: var(--space-2); + } + } + + [data-slot="icon"] { + width: var(--space-4); + height: var(--space-4); + transition: transform 0.2s ease; + } + + &[data-rotate] [data-slot="icon"] { + transform: rotate(180deg); + } +} diff --git a/cloud/web/src/ui/style/component/dialog.css b/cloud/web/src/ui/style/component/dialog.css new file mode 100644 index 000000000..59867818f --- /dev/null +++ b/cloud/web/src/ui/style/component/dialog.css @@ -0,0 +1,84 @@ +[data-component="dialog-overlay"] { + pointer-events: none !important; + position: fixed; + inset: 0; + animation-name: fadeOut; + animation-duration: 200ms; + animation-timing-function: ease; + opacity: 0; + backdrop-filter: blur(2px); + + &[data-expanded] { + animation-name: fadeIn; + opacity: 1; + pointer-events: auto !important; + } +} + +[data-component="dialog-center"] { + position: fixed; + inset: 0; + padding-top: 10vh; + justify-content: center; + pointer-events: none; + + [data-slot="content"] { + width: 45rem; + margin: 0 auto; + transition: 150ms width; + background-color: var(--color-bg); + border-width: 2px; + border-color: var(--color-border); + overflow: hidden; + display: flex; + flex-direction: column; + gap: var(--space-3); + outline: none; + animation-duration: 1ms; + animation-name: zoomOut; + animation-timing-function: ease; + + box-shadow: 8px 8px 0px 0px var(--color-gray-4); + + &[data-expanded] { + animation-name: zoomIn; + } + + &[data-transition] { + animation-duration: 200ms; + } + + &[data-size="sm"] { + width: 30rem; + } + + [data-slot="header"] { + display: flex; + padding: var(--space-4) var(--space-4) 0; + + [data-slot="title"] { + } + } + + [data-slot="main"] { + padding: 0 var(--space-4); + + &:has([data-slot="options"]) { + padding: 0; + display: flex; + flex-direction: column; + gap: var(--space-4); + } + } + + [data-slot="input"] { + } + + [data-slot="footer"] { + padding: var(--space-4); + display: flex; + gap: var(--space-4); + justify-content: end; + } + } +} diff --git a/cloud/web/src/ui/style/component/input.css b/cloud/web/src/ui/style/component/input.css new file mode 100644 index 000000000..59535d763 --- /dev/null +++ b/cloud/web/src/ui/style/component/input.css @@ -0,0 +1,34 @@ +[data-component="input"] { + font-size: var(--font-size-md); + background: transparent; + caret-color: var(--color-accent); + font-family: var(--font-mono); + height: var(--space-11); + padding: 0 var(--space-4); + width: 100%; + resize: none; + border: 2px solid var(--color-border); + + &::placeholder { + color: var(--color-text-dimmed); + opacity: 0.75; + } + + &:focus { + outline: 0; + } + + &[data-size="sm"] { + height: var(--space-9); + padding: 0 var(--space-3); + font-size: var(--font-size-xs); + } + + &[data-size="md"] { + } + + &[data-size="lg"] { + height: var(--space-12); + font-size: var(--font-size-lg); + } +} diff --git a/cloud/web/src/ui/style/component/label.css b/cloud/web/src/ui/style/component/label.css new file mode 100644 index 000000000..e0dd5fef4 --- /dev/null +++ b/cloud/web/src/ui/style/component/label.css @@ -0,0 +1,17 @@ +[data-component="label"] { + letter-spacing: -0.03125rem; + text-transform: uppercase; + color: var(--color-text-dimmed); + font-weight: 500; + font-size: var(--font-size-md); + + &[data-size="sm"] { + font-size: var(--font-size-sm); + } + &[data-size="md"] { + } + &[data-size="lg"] { + font-size: var(--font-size-lg); + } +} + diff --git a/cloud/web/src/ui/style/component/title-bar.css b/cloud/web/src/ui/style/component/title-bar.css new file mode 100644 index 000000000..7ee32bfdc --- /dev/null +++ b/cloud/web/src/ui/style/component/title-bar.css @@ -0,0 +1,32 @@ +[data-component="title-bar"] { + display: flex; + align-items: center; + justify-content: space-between; + height: 72px; + padding: 0 var(--space-4); + border-bottom: 2px solid var(--color-border); + + [data-slot="left"] { + display: flex; + flex-direction: column; + gap: var(--space-1-5); + + h1 { + letter-spacing: -0.03125rem; + font-size: var(--font-size-xl); + text-transform: uppercase; + font-weight: 600; + } + + p { + color: var(--color-text-dimmed); + } + } + +} + +@media (max-width: 40rem) { + [data-component="title-bar"] { + display: none; + } +} diff --git a/cloud/web/src/ui/style/index.css b/cloud/web/src/ui/style/index.css new file mode 100644 index 000000000..117f596d0 --- /dev/null +++ b/cloud/web/src/ui/style/index.css @@ -0,0 +1,50 @@ +/* tokens */ +@import "./token/color.css"; +@import "./token/reset.css"; +@import "./token/animation.css"; +@import "./token/font.css"; +@import "./token/space.css"; + +/* components */ +@import "./component/label.css"; +@import "./component/input.css"; +@import "./component/button.css"; +@import "./component/dialog.css"; +@import "./component/title-bar.css"; + +body { + font-family: var(--font-mono); + line-height: 1; + color: var(--color-text); + background-color: var(--color-bg); + cursor: default; + user-select: none; + text-underline-offset: 0.1875rem; +} + +a { + text-decoration: underline; + &:active { + color: var(--color-text-accent); + } +} + +::selection { + background-color: var(--color-text-accent-invert); +} + +/* Responsive utilities */ +[data-max-width] { + width: 100%; + + & > * { + max-width: 90rem; + margin-left: auto; + margin-right: auto; + width: 100%; + } + + &[data-max-width-64] > * { + max-width: 64rem; + } +} diff --git a/cloud/web/src/ui/style/token/animation.css b/cloud/web/src/ui/style/token/animation.css new file mode 100644 index 000000000..a8edfeff5 --- /dev/null +++ b/cloud/web/src/ui/style/token/animation.css @@ -0,0 +1,23 @@ +@keyframes zoomIn { + from { + opacity: 0; + transform: scale(0.95); + } + + to { + opacity: 1; + transform: scale(1); + } +} + +@keyframes zoomOut { + from { + opacity: 1; + transform: scale(1); + } + + to { + opacity: 0; + transform: scale(0.95); + } +} diff --git a/cloud/web/src/ui/style/token/color.css b/cloud/web/src/ui/style/token/color.css new file mode 100644 index 000000000..af0c46f3b --- /dev/null +++ b/cloud/web/src/ui/style/token/color.css @@ -0,0 +1,88 @@ +:root { + --color-white: hsl(0, 0%, 100%); + --color-gray-1: hsl(224, 20%, 94%); + --color-gray-2: hsl(224, 6%, 77%); + --color-gray-3: hsl(224, 6%, 56%); + --color-gray-4: hsl(224, 7%, 36%); + --color-gray-5: hsl(224, 10%, 23%); + --color-gray-6: hsl(224, 14%, 16%); + --color-black: hsl(224, 10%, 10%); + + --hue-orange: 41; + --color-orange-low: hsl(var(--hue-orange), 39%, 22%); + --color-orange: hsl(var(--hue-orange), 82%, 63%); + --color-orange-high: hsl(var(--hue-orange), 82%, 87%); + --hue-green: 101; + --color-green-low: hsl(var(--hue-green), 39%, 22%); + --color-green: hsl(var(--hue-green), 82%, 63%); + --color-green-high: hsl(var(--hue-green), 82%, 80%); + --hue-blue: 234; + --color-blue-low: hsl(var(--hue-blue), 54%, 20%); + --color-blue: hsl(var(--hue-blue), 100%, 60%); + --color-blue-high: hsl(var(--hue-blue), 100%, 87%); + --hue-purple: 281; + --color-purple-low: hsl(var(--hue-purple), 39%, 22%); + --color-purple: hsl(var(--hue-purple), 82%, 63%); + --color-purple-high: hsl(var(--hue-purple), 82%, 89%); + --hue-red: 339; + --color-red-low: hsl(var(--hue-red), 39%, 22%); + --color-red: hsl(var(--hue-red), 82%, 63%); + --color-red-high: hsl(var(--hue-red), 82%, 87%); + + --color-accent-low: hsl(13, 75%, 30%); + --color-accent: hsl(13, 88%, 57%); + --color-accent-high: hsl(13, 100%, 78%); + + --color-text: var(--color-gray-1); + --color-text-dimmed: var(--color-gray-3); + --color-text-accent: var(--color-accent); + --color-text-invert: var(--color-black); + --color-text-accent-invert: var(--color-accent-high); + --color-bg: var(--color-black); + --color-bg-surface: var(--color-gray-5); + --color-bg-accent: var(--color-accent-high); + --color-border: var(--color-gray-2); + + --color-backdrop-overlay: hsla(223, 13%, 10%, 0.66); +} + +:root[data-color-mode="light"] { + --color-white: hsl(224, 10%, 10%); + --color-gray-1: hsl(224, 14%, 16%); + --color-gray-2: hsl(224, 10%, 23%); + --color-gray-3: hsl(224, 7%, 36%); + --color-gray-4: hsl(224, 6%, 56%); + --color-gray-5: hsl(224, 6%, 77%); + --color-gray-6: hsl(224, 20%, 94%); + --color-gray-7: hsl(224, 19%, 97%); + --color-black: hsl(0, 0%, 100%); + + --color-orange-high: hsl(var(--hue-orange), 80%, 25%); + --color-orange: hsl(var(--hue-orange), 90%, 60%); + --color-orange-low: hsl(var(--hue-orange), 90%, 88%); + --color-green-high: hsl(var(--hue-green), 80%, 22%); + --color-green: hsl(var(--hue-green), 90%, 46%); + --color-green-low: hsl(var(--hue-green), 85%, 90%); + --color-blue-high: hsl(var(--hue-blue), 80%, 30%); + --color-blue: hsl(var(--hue-blue), 90%, 60%); + --color-blue-low: hsl(var(--hue-blue), 88%, 90%); + --color-purple-high: hsl(var(--hue-purple), 90%, 30%); + --color-purple: hsl(var(--hue-purple), 90%, 60%); + --color-purple-low: hsl(var(--hue-purple), 80%, 90%); + --color-red-high: hsl(var(--hue-red), 80%, 30%); + --color-red: hsl(var(--hue-red), 90%, 60%); + --color-red-low: hsl(var(--hue-red), 80%, 90%); + + --color-accent-high: hsl(13, 75%, 26%); + --color-accent: hsl(13, 88%, 60%); + --color-accent-low: hsl(13, 100%, 89%); + + --color-text-accent: var(--color-accent); + --color-text-dimmed: var(--color-gray-4); + --color-text-invert: var(--color-black); + --color-text-accent-invert: var(--color-accent-low); + --color-bg-surface: var(--color-gray-6); + --color-bg-accent: var(--color-accent); + + --color-backdrop-overlay: hsla(225, 9%, 36%, 0.66); +} diff --git a/cloud/web/src/ui/style/token/font.css b/cloud/web/src/ui/style/token/font.css new file mode 100644 index 000000000..24b2db3f2 --- /dev/null +++ b/cloud/web/src/ui/style/token/font.css @@ -0,0 +1,20 @@ +:root { + --font-size-2xs: 0.6875rem; + --font-size-xs: 0.75rem; + --font-size-sm: 0.8125rem; + --font-size-md: 0.9375rem; + --font-size-lg: 1.125rem; + --font-size-xl: 1.25rem; + --font-size-2xl: 1.5rem; + --font-size-3xl: 1.875rem; + --font-size-4xl: 2.25rem; + --font-size-5xl: 3rem; + --font-size-6xl: 3.75rem; + --font-size-7xl: 4.5rem; + --font-size-8xl: 6rem; + --font-size-9xl: 8rem; + --font-mono: IBM Plex Mono, monospace; + --font-sans: Rubik, sans-serif; + + --font-line-height: 1.75; +} diff --git a/cloud/web/src/ui/style/token/reset.css b/cloud/web/src/ui/style/token/reset.css new file mode 100644 index 000000000..f4aa1a0a9 --- /dev/null +++ b/cloud/web/src/ui/style/token/reset.css @@ -0,0 +1,212 @@ +* { + margin: 0; + padding: 0; + font: inherit; +} + +*, +*::before, +*::after { + box-sizing: border-box; + border-width: 0; + border-style: solid; + border-color: var(--global-color-border, currentColor); +} + +html { + line-height: 1.5; + --font-fallback: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, + "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, + "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; + -webkit-text-size-adjust: 100%; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + -moz-tab-size: 4; + tab-size: 4; + font-family: var(--global-font-body, var(--font-fallback)); +} + +hr { + height: 0; + color: inherit; + border-top-width: 1px; +} + +body { + height: 100%; + line-height: inherit; +} + +img { + border-style: none; +} + +img, +svg, +video, +canvas, +audio, +iframe, +embed, +object { + display: block; + vertical-align: middle; +} + +img, +video { + max-width: 100%; + height: auto; +} + +p, +h1, +h2, +h3, +h4, +h5, +h6 { + overflow-wrap: break-word; +} + +ol, +ul { + list-style: none; +} + +code, +kbd, +pre, +samp { + font-size: 1em; +} + +button, +[type="button"], +[type="reset"], +[type="submit"] { + -webkit-appearance: button; + background-color: transparent; + background-image: none; +} + +button, +input, +optgroup, +select, +textarea { + color: inherit; +} + +button, +select { + text-transform: none; +} + +table { + text-indent: 0; + border-color: inherit; + border-collapse: collapse; +} + +input::placeholder, +textarea::placeholder { + opacity: 1; + color: var(--global-color-placeholder, #9ca3af); +} + +textarea { + resize: vertical; +} + +summary { + display: list-item; +} + +small { + font-size: 80%; +} + +sub, +sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; +} + +sub { + bottom: -0.25em; +} + +sup { + top: -0.5em; +} + +dialog { + padding: 0; +} + +a { + color: inherit; + text-decoration: inherit; +} + +abbr:where([title]) { + text-decoration: underline dotted; +} + +b, +strong { + font-weight: bolder; +} + +code, +kbd, +samp, +pre { + font-size: 1em; + --font-mono-fallback: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, + "Liberation Mono", "Courier New"; + font-family: var(--global-font-mono, var(--font-fallback)); +} + +input[type="text"], +input[type="email"], +input[type="search"], +input[type="password"] { + -webkit-appearance: none; + -moz-appearance: none; +} + +input[type="search"] { + -webkit-appearance: textfield; + outline-offset: -2px; +} + +::-webkit-search-decoration, +::-webkit-search-cancel-button { + -webkit-appearance: none; +} + +::-webkit-file-upload-button { + -webkit-appearance: button; + font: inherit; +} + +input[type="number"]::-webkit-inner-spin-button, +input[type="number"]::-webkit-outer-spin-button { + height: auto; +} + +input[type="number"] { + -moz-appearance: textfield; +} + +:-moz-ui-invalid { + box-shadow: none; +} + +:-moz-focusring { + outline: auto; +} diff --git a/cloud/web/src/ui/style/token/space.css b/cloud/web/src/ui/style/token/space.css new file mode 100644 index 000000000..b1e492f49 --- /dev/null +++ b/cloud/web/src/ui/style/token/space.css @@ -0,0 +1,38 @@ +:root { + --space-0: 0; + --space-px: 1px; + --space-0-5: 0.125rem; + --space-1: 0.25rem; + --space-1-5: 0.375rem; + --space-2: 0.5rem; + --space-2-5: 0.625rem; + --space-3: 0.75rem; + --space-3-5: 0.875rem; + --space-4: 1rem; + --space-4-5: 1.125rem; + --space-5: 1.25rem; + --space-6: 1.5rem; + --space-7: 1.75rem; + --space-8: 2rem; + --space-9: 2.25rem; + --space-10: 2.5rem; + --space-11: 2.75rem; + --space-12: 3rem; + --space-14: 3.5rem; + --space-16: 4rem; + --space-20: 5rem; + --space-24: 6rem; + --space-28: 7rem; + --space-32: 8rem; + --space-36: 9rem; + --space-40: 10rem; + --space-44: 11rem; + --space-48: 12rem; + --space-52: 13rem; + --space-56: 14rem; + --space-60: 15rem; + --space-64: 16rem; + --space-72: 18rem; + --space-80: 20rem; + --space-96: 24rem; +} diff --git a/cloud/web/src/ui/svg/icons.tsx b/cloud/web/src/ui/svg/icons.tsx new file mode 100644 index 000000000..c09bbc47a --- /dev/null +++ b/cloud/web/src/ui/svg/icons.tsx @@ -0,0 +1,1292 @@ +import { JSX } from "solid-js" + +export function IconPencilSquare(props: JSX.SvgSVGAttributes) { + return ( + + + + + ) +} + +export function IconHome(props: JSX.SvgSVGAttributes) { + return ( + + + + + ) +} + +export function IconPlus(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} + +export function IconDocument(props: JSX.SvgSVGAttributes) { + return ( + + + + + ) +} + +export function IconChat(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} + +export function IconBell(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} + +export function IconCheck(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} + +export function IconChevronDown(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} + +export function IconChevronUp(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} + +export function IconChevronLeft(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} + +export function IconChevronRight(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} + +export function IconTrash(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} + +export function IconUser(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} + +export function IconCog(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} + +export function IconExclamationCircle( + props: JSX.SvgSVGAttributes, +) { + return ( + + + + ) +} + +export function IconInformationCircle( + props: JSX.SvgSVGAttributes, +) { + return ( + + + + ) +} + +export function IconArrowPath(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} + +export function IconArrowUpRight(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} + +export function IconEllipsisVertical( + props: JSX.SvgSVGAttributes, +) { + return ( + + + + ) +} + +export function IconEllipsisHorizontal( + props: JSX.SvgSVGAttributes, +) { + return ( + + + + ) +} + +export function IconXMark(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} + +export function IconAcademicCap(props: JSX.SvgSVGAttributes) { + return ( + + + + + + ) +} + +export function IconBolt(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} + +export function IconCalendar(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} + +export function IconCamera(props: JSX.SvgSVGAttributes) { + return ( + + + + + ) +} + +export function IconClock(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} + +export function IconCloud(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} + +export function IconCreditCard(props: JSX.SvgSVGAttributes) { + return ( + + + + + ) +} + +export function IconEnvelope(props: JSX.SvgSVGAttributes) { + return ( + + + + + ) +} + +export function IconEye(props: JSX.SvgSVGAttributes) { + return ( + + + + + ) +} + +export function IconFlag(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} + +export function IconFolder(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} + +export function IconGlobe(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} + +export function IconHeart(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} + +export function IconKey(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} + +export function IconLink(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} + +export function IconLock(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} + +export function IconMap(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} + +export function IconMicrophone(props: JSX.SvgSVGAttributes) { + return ( + + + + + ) +} + +export function IconPhone(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} + +export function IconPhoto(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} + +export function IconQuestionMarkCircle( + props: JSX.SvgSVGAttributes, +) { + return ( + + + + ) +} + +export function IconMagnifyingGlass( + props: JSX.SvgSVGAttributes, +) { + return ( + + + + ) +} + +export function IconShieldCheck(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} + +export function IconShoppingCart(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} + +export function IconStar(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} + +export function IconTag(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} + +export function IconUserCircle(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} + +export function IconVideoCamera(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} + +export function IconWifi(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} + +export function IconAdjustmentsVertical( + props: JSX.SvgSVGAttributes, +) { + return ( + + + + ) +} + +export function IconArchiveBox(props: JSX.SvgSVGAttributes) { + return ( + + + + + ) +} + +export function IconArrowDown(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} + +export function IconArrowLeft(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} + +export function IconArrowLongDown(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} + +export function IconArrowLongLeft(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} + +export function IconArrowLongRight(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} + +export function IconArrowLongUp(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} + +export function IconArrowRight(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} + +export function IconArrowSmallDown(props: JSX.SvgSVGAttributes) { + return ( + + + + + ) +} + +export function IconArrowSmallLeft(props: JSX.SvgSVGAttributes) { + return ( + + + + + ) +} + +export function IconArrowSmallRight( + props: JSX.SvgSVGAttributes, +) { + return ( + + + + + ) +} + +export function IconArrowSmallUp(props: JSX.SvgSVGAttributes) { + return ( + + + + + ) +} + +export function IconArrowTopRightOnSquare( + props: JSX.SvgSVGAttributes, +) { + return ( + + + + ) +} + +export function IconArrowTrendingDown( + props: JSX.SvgSVGAttributes, +) { + return ( + + + + + ) +} + +export function IconArrowTrendingUp( + props: JSX.SvgSVGAttributes, +) { + return ( + + + + ) +} + +export function IconArrowUp(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} + +export function IconArrowUpCircle(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} + +export function IconArrowUpLeft(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} + +export function IconArrowUpOnSquare( + props: JSX.SvgSVGAttributes, +) { + return ( + + + + + ) +} + +export function IconArrowUpTray(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} + +export function IconArrowsPointingIn( + props: JSX.SvgSVGAttributes, +) { + return ( + + + + ) +} + +export function IconArrowsPointingOut( + props: JSX.SvgSVGAttributes, +) { + return ( + + + + ) +} + +export function IconArrowsRightLeft( + props: JSX.SvgSVGAttributes, +) { + return ( + + + + ) +} + +export function IconBars3BottomLeft( + props: JSX.SvgSVGAttributes, +) { + return ( + + + + ) +} diff --git a/cloud/web/src/ui/svg/index.tsx b/cloud/web/src/ui/svg/index.tsx new file mode 100644 index 000000000..23dd74c6e --- /dev/null +++ b/cloud/web/src/ui/svg/index.tsx @@ -0,0 +1,67 @@ +import { JSX } from "solid-js" + +export function IconLogomark(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} + +export function IconLogo(props: JSX.SvgSVGAttributes) { + return ( + + + + + + + + + + + + + + + ) +} -- cgit v1.2.3