diff --git a/crates/weaver-app/Cargo.toml b/crates/weaver-app/Cargo.toml index a97694a..73169af 100644 --- a/crates/weaver-app/Cargo.toml +++ b/crates/weaver-app/Cargo.toml @@ -57,7 +57,7 @@ jacquard-identity = { workspace = true } jacquard-axum = { workspace = true, optional = true } weaver-api = { path = "../weaver-api", features = ["com_whtwnd"] } markdown-weaver = { workspace = true } -weaver-renderer = { path = "../weaver-renderer" } +weaver-renderer = { path = "../weaver-renderer", features = ["themes"] } n0-future = { workspace = true } dioxus-primitives = { git = "https://github.com/DioxusLabs/components", version = "0.0.1", default-features = false, features = ["router"] } axum = { version = "0.8.6", optional = true } diff --git a/crates/weaver-app/src/components/checkbox/component.rs b/crates/weaver-app/src/components/checkbox/component.rs new file mode 100644 index 0000000..782cb19 --- /dev/null +++ b/crates/weaver-app/src/components/checkbox/component.rs @@ -0,0 +1,28 @@ +use dioxus::prelude::*; +use dioxus_primitives::checkbox::{self, CheckboxProps}; + +#[component] +pub fn Checkbox(props: CheckboxProps) -> Element { + rsx! { + document::Link { rel: "stylesheet", href: asset!("./style.css") } + checkbox::Checkbox { + class: "checkbox", + checked: props.checked, + default_checked: props.default_checked, + required: props.required, + disabled: props.disabled, + name: props.name, + value: props.value, + on_checked_change: props.on_checked_change, + attributes: props.attributes, + checkbox::CheckboxIndicator { class: "checkbox-indicator", + svg { + class: "checkbox-check-icon", + view_box: "0 0 24 24", + xmlns: "http://www.w3.org/2000/svg", + path { d: "M5 13l4 4L19 7" } + } + } + } + } +} diff --git a/crates/weaver-app/src/components/checkbox/mod.rs b/crates/weaver-app/src/components/checkbox/mod.rs new file mode 100644 index 0000000..9a8ae55 --- /dev/null +++ b/crates/weaver-app/src/components/checkbox/mod.rs @@ -0,0 +1,2 @@ +mod component; +pub use component::*; \ No newline at end of file diff --git a/crates/weaver-app/src/components/checkbox/style.css b/crates/weaver-app/src/components/checkbox/style.css new file mode 100644 index 0000000..5d236a3 --- /dev/null +++ b/crates/weaver-app/src/components/checkbox/style.css @@ -0,0 +1,39 @@ +.checkbox { + width: 1rem; + height: 1rem; + box-sizing: border-box; + padding: 0; + border: none; + border-radius: 4px; + margin: 0; + background-color: var(--primary-color-3); + box-shadow: inset 0 0 0 1px var(--primary-color-7); + color: var(--secondary-color-4); + cursor: pointer; +} + +.checkbox-indicator { + display: flex; + align-items: center; + justify-content: center; +} + +.checkbox[data-state="checked"] { + background-color: var(--secondary-color-2); + box-shadow: none; + color: var(--primary-color); +} + +.checkbox:focus-visible { + box-shadow: 0 0 0 2px var(--focused-border-color); +} + +.checkbox-check-icon { + width: 1rem; + height: 1rem; + fill: none; + stroke: currentcolor; + stroke-linecap: round; + stroke-linejoin: round; + stroke-width: 2; +} diff --git a/crates/weaver-app/src/components/hover_card/component.rs b/crates/weaver-app/src/components/hover_card/component.rs new file mode 100644 index 0000000..9ba2e77 --- /dev/null +++ b/crates/weaver-app/src/components/hover_card/component.rs @@ -0,0 +1,47 @@ +use dioxus::prelude::*; +use dioxus_primitives::hover_card::{ + self, HoverCardContentProps, HoverCardProps, HoverCardTriggerProps, +}; + +#[component] +pub fn HoverCard(props: HoverCardProps) -> Element { + rsx! { + document::Link { rel: "stylesheet", href: asset!("./style.css") } + hover_card::HoverCard { + class: "hover-card", + open: props.open, + default_open: props.default_open, + on_open_change: props.on_open_change, + disabled: props.disabled, + attributes: props.attributes, + {props.children} + } + } +} + +#[component] +pub fn HoverCardTrigger(props: HoverCardTriggerProps) -> Element { + rsx! { + hover_card::HoverCardTrigger { + class: "hover-card-trigger", + id: props.id, + attributes: props.attributes, + {props.children} + } + } +} + +#[component] +pub fn HoverCardContent(props: HoverCardContentProps) -> Element { + rsx! { + hover_card::HoverCardContent { + class: "hover-card-content", + side: props.side, + align: props.align, + id: props.id, + force_mount: props.force_mount, + attributes: props.attributes, + {props.children} + } + } +} diff --git a/crates/weaver-app/src/components/hover_card/mod.rs b/crates/weaver-app/src/components/hover_card/mod.rs new file mode 100644 index 0000000..9a8ae55 --- /dev/null +++ b/crates/weaver-app/src/components/hover_card/mod.rs @@ -0,0 +1,2 @@ +mod component; +pub use component::*; \ No newline at end of file diff --git a/crates/weaver-app/src/components/hover_card/style.css b/crates/weaver-app/src/components/hover_card/style.css new file mode 100644 index 0000000..3d95afb --- /dev/null +++ b/crates/weaver-app/src/components/hover_card/style.css @@ -0,0 +1,118 @@ +/* Hover Card Styles */ +.hover-card { + position: relative; + display: inline-block; +} + +.hover-card-trigger { + display: inline-block; +} + +.hover-card-content { + position: absolute; + z-index: 1000; + min-width: 200px; + padding: 5px; + border: 1px solid var(--light, var(--primary-color-6)) + var(--dark, var(--primary-color-7)); + border-radius: 0.5rem; + animation: hover-card-fade-in 0.1s ease-out; + background: var(--light, var(--primary-color)) + var(--dark, var(--primary-color-5)); + + /* Semi transparent shadow effect in light mode */ + box-shadow: var(--light, 0 2px 10px #0000001a) var(--dark, none); +} + +/* Positioning based on side */ +.hover-card-content[data-side="top"] { + position: absolute; + bottom: 100%; + left: 50%; + margin-bottom: 10px; + transform: translateX(-50%); +} + +.hover-card-content[data-side="right"] { + position: absolute; + top: 50%; + left: 100%; + margin-left: 10px; + transform: translateY(-50%); +} + +.hover-card-content[data-side="bottom"] { + position: absolute; + top: 100%; + left: 50%; + margin-top: 10px; + transform: translateX(-50%); +} + +.hover-card-content[data-side="left"] { + position: absolute; + top: 50%; + right: 100%; + margin-right: 10px; + transform: translateY(-50%); +} + +/* Alignment styles for top and bottom */ +.hover-card-content[data-side="top"][data-align="start"], +.hover-card-content[data-side="bottom"][data-align="start"] { + left: 0; + transform: none; +} + +.hover-card-content[data-side="top"][data-align="center"], +.hover-card-content[data-side="bottom"][data-align="center"] { + left: 50%; + transform: translateX(-50%); +} + +.hover-card-content[data-side="top"][data-align="end"], +.hover-card-content[data-side="bottom"][data-align="end"] { + right: 0; + left: auto; + transform: none; +} + +/* Alignment styles for left and right */ +.hover-card-content[data-side="left"][data-align="start"], +.hover-card-content[data-side="right"][data-align="start"] { + top: 0; + transform: none; +} + +.hover-card-content[data-side="left"][data-align="center"], +.hover-card-content[data-side="right"][data-align="center"] { + top: 50%; + transform: translateY(-50%); +} + +.hover-card-content[data-side="left"][data-align="end"], +.hover-card-content[data-side="right"][data-align="end"] { + top: auto; + bottom: 0; + transform: none; +} + +/* Animation */ +@keyframes hover-card-fade-in { + from { + opacity: 0; + } + + to { + opacity: 1; + } +} + +/* State styles */ +.hover-card[data-disabled="true"] .hover-card-trigger { + color: var(--secondary-color-5); +} + +.hover-card-content[data-state="closed"] { + display: none; +} diff --git a/crates/weaver-app/src/components/mod.rs b/crates/weaver-app/src/components/mod.rs index d5d73ea..b37aff1 100644 --- a/crates/weaver-app/src/components/mod.rs +++ b/crates/weaver-app/src/components/mod.rs @@ -351,3 +351,11 @@ pub use entry_actions::EntryActions; pub use notebook_actions::NotebookActions; pub use profile_actions::{ProfileActions, ProfileActionsMenubar}; pub mod toast; +pub mod hover_card; +pub mod switch; +pub mod select; +pub mod radio_group; +pub mod slider; +pub mod checkbox; +pub mod toggle_group; +pub mod tooltip; diff --git a/crates/weaver-app/src/components/radio_group/component.rs b/crates/weaver-app/src/components/radio_group/component.rs new file mode 100644 index 0000000..0607ca5 --- /dev/null +++ b/crates/weaver-app/src/components/radio_group/component.rs @@ -0,0 +1,36 @@ +use dioxus::prelude::*; +use dioxus_primitives::radio_group::{self, RadioGroupProps, RadioItemProps}; + +#[component] +pub fn RadioGroup(props: RadioGroupProps) -> Element { + rsx! { + document::Link { rel: "stylesheet", href: asset!("./style.css") } + radio_group::RadioGroup { + class: "radio-group", + value: props.value, + default_value: props.default_value, + on_value_change: props.on_value_change, + disabled: props.disabled, + required: props.required, + name: props.name, + horizontal: props.horizontal, + roving_loop: props.roving_loop, + attributes: props.attributes, + {props.children} + } + } +} + +#[component] +pub fn RadioItem(props: RadioItemProps) -> Element { + rsx! { + radio_group::RadioItem { + class: "radio-item", + value: props.value, + index: props.index, + disabled: props.disabled, + attributes: props.attributes, + {props.children} + } + } +} diff --git a/crates/weaver-app/src/components/radio_group/mod.rs b/crates/weaver-app/src/components/radio_group/mod.rs new file mode 100644 index 0000000..9a8ae55 --- /dev/null +++ b/crates/weaver-app/src/components/radio_group/mod.rs @@ -0,0 +1,2 @@ +mod component; +pub use component::*; \ No newline at end of file diff --git a/crates/weaver-app/src/components/radio_group/style.css b/crates/weaver-app/src/components/radio_group/style.css new file mode 100644 index 0000000..e144ed7 --- /dev/null +++ b/crates/weaver-app/src/components/radio_group/style.css @@ -0,0 +1,48 @@ +.radio-group { + display: flex; + flex-direction: column; + gap: .75rem; +} + +.radio-item { + display: flex; + flex-direction: row; + align-items: center; + padding: 0; + border: none; + background-color: transparent; + color: var(--secondary-color-4); + font-size: 14px; + gap: .75rem; + + &::before { + display: block; + width: 1rem; + height: 1rem; + box-sizing: border-box; + border-radius: 1.5rem; + background: var(--light, var(--primary-color)) var(--dark, var(--primary-color-3)); + box-shadow: 0 0 0 1px var(--light, var(--primary-color-6)) + var(--dark, var(--primary-color-7)); + content: ""; + cursor: pointer; + } + + &:focus-visible { + outline: none; + } + + &:focus-visible::before { + box-shadow: 0 0 0 2px var(--focused-border-color); + } + + &[data-state="checked"]::before { + border: 0.25rem solid var(--light, var(--primary-color)) var(--dark, var(--primary-color-3)); + background: var(--secondary-color-4); + } + + &[data-disabled="true"]::before { + cursor: not-allowed; + opacity: 0.5; + } +} diff --git a/crates/weaver-app/src/components/select/component.rs b/crates/weaver-app/src/components/select/component.rs new file mode 100644 index 0000000..88e70f0 --- /dev/null +++ b/crates/weaver-app/src/components/select/component.rs @@ -0,0 +1,116 @@ +use dioxus::prelude::*; +use dioxus_primitives::select::{ + self, SelectGroupLabelProps, SelectGroupProps, SelectListProps, SelectOptionProps, SelectProps, + SelectTriggerProps, SelectValueProps, +}; + +#[component] +pub fn Select(props: SelectProps) -> Element { + rsx! { + document::Link { rel: "stylesheet", href: asset!("./style.css") } + select::Select { + class: "select", + value: props.value, + default_value: props.default_value, + on_value_change: props.on_value_change, + disabled: props.disabled, + name: props.name, + placeholder: props.placeholder, + roving_loop: props.roving_loop, + typeahead_timeout: props.typeahead_timeout, + attributes: props.attributes, + {props.children} + } + } +} + +#[component] +pub fn SelectTrigger(props: SelectTriggerProps) -> Element { + rsx! { + select::SelectTrigger { class: "select-trigger", attributes: props.attributes, + {props.children} + svg { + class: "select-expand-icon", + view_box: "0 0 24 24", + xmlns: "http://www.w3.org/2000/svg", + polyline { points: "6 9 12 15 18 9" } + } + } + } +} + +#[component] +pub fn SelectValue(props: SelectValueProps) -> Element { + rsx! { + select::SelectValue { attributes: props.attributes } + } +} + +#[component] +pub fn SelectList(props: SelectListProps) -> Element { + rsx! { + select::SelectList { + class: "select-list", + id: props.id, + attributes: props.attributes, + {props.children} + } + } +} + +#[component] +pub fn SelectGroup(props: SelectGroupProps) -> Element { + rsx! { + select::SelectGroup { + class: "select-group", + disabled: props.disabled, + id: props.id, + attributes: props.attributes, + {props.children} + } + } +} + +#[component] +pub fn SelectGroupLabel(props: SelectGroupLabelProps) -> Element { + rsx! { + select::SelectGroupLabel { + class: "select-group-label", + id: props.id, + attributes: props.attributes, + {props.children} + } + } +} + +#[component] +pub fn SelectOption(props: SelectOptionProps) -> Element { + rsx! { + select::SelectOption:: { + class: "select-option", + value: props.value, + text_value: props.text_value, + disabled: props.disabled, + id: props.id, + index: props.index, + aria_label: props.aria_label, + aria_roledescription: props.aria_roledescription, + attributes: props.attributes, + {props.children} + } + } +} + +#[component] +pub fn SelectItemIndicator() -> Element { + rsx! { + select::SelectItemIndicator { + svg { + class: "select-check-icon", + view_box: "0 0 24 24", + xmlns: "http://www.w3.org/2000/svg", + path { d: "M5 13l4 4L19 7" } + } + } + } +} diff --git a/crates/weaver-app/src/components/select/mod.rs b/crates/weaver-app/src/components/select/mod.rs new file mode 100644 index 0000000..9a8ae55 --- /dev/null +++ b/crates/weaver-app/src/components/select/mod.rs @@ -0,0 +1,2 @@ +mod component; +pub use component::*; \ No newline at end of file diff --git a/crates/weaver-app/src/components/select/style.css b/crates/weaver-app/src/components/select/style.css new file mode 100644 index 0000000..5a98dd3 --- /dev/null +++ b/crates/weaver-app/src/components/select/style.css @@ -0,0 +1,155 @@ +.select { + position: relative; +} + +.select-trigger { + position: relative; + display: flex; + box-sizing: border-box; + flex-direction: row; + align-items: center; + justify-content: space-between; + padding: 0.25rem; + padding: 8px 12px; + border: none; + border-radius: 0.5rem; + border-radius: calc(0.5rem); + background: none; + background: var(--light, var(--primary-color)) + var(--dark, var(--primary-color-3)); + box-shadow: inset 0 0 0 1px var(--light, var(--primary-color-6)) + var(--dark, var(--primary-color-7)); + color: var(--secondary-color-4); + cursor: pointer; + gap: 0.25rem; + transition: background-color 100ms ease-out; +} + +.select-trigger span[data-placeholder="true"] { + color: var(--secondary-color-5); +} + +.select[data-state="open"] .select-trigger { + pointer-events: none; +} + +.select-expand-icon { + width: 20px; + height: 20px; + fill: none; + stroke: var(--primary-color-7); + stroke-linecap: round; + stroke-linejoin: round; + stroke-width: 2; +} + +.select-check-icon { + width: 1rem; + height: 1rem; + fill: none; + stroke: var(--secondary-color-5); + stroke-linecap: round; + stroke-linejoin: round; + stroke-width: 2; +} + +.select[data-disabled="true"] .select-trigger { + color: var(--secondary-color-5); + cursor: not-allowed; +} + +.select-trigger:hover:not([data-disabled="true"]), +.select-trigger:focus-visible { + background: var(--light, var(--primary-color-4)) + var(--dark, var(--primary-color-5)); + color: var(--secondary-color-1); + outline: none; +} + +.select-list { + position: absolute; + z-index: 1000; + top: 100%; + left: 0; + min-width: 100%; + box-sizing: border-box; + padding: 0.25rem; + border-radius: 0.5rem; + margin-top: 0.25rem; + background: var(--light, var(--primary-color)) + var(--dark, var(--primary-color-5)); + box-shadow: inset 0 0 0 1px var(--light, var(--primary-color-6)) + var(--dark, var(--primary-color-7)); + opacity: 0; + pointer-events: none; + transform-origin: top; + will-change: transform, opacity; +} + +.select-list[data-state="closed"] { + animation: select-list-animate-out 150ms ease-in forwards; + pointer-events: none; +} + +@keyframes select-list-animate-out { + 0% { + opacity: 1; + transform: scale(1) translateY(0); + } + + 100% { + opacity: 0; + transform: scale(0.95) translateY(-2px); + } +} + +.select-list[data-state="open"] { + animation: select-list-animate-in 150ms ease-out forwards; + pointer-events: auto; +} + +@keyframes select-list-animate-in { + 0% { + opacity: 0; + transform: scale(0.95) translateY(-2px); + } + + 100% { + opacity: 1; + transform: scale(1) translateY(0); + } +} + +.select-option { + display: flex; + align-items: center; + justify-content: space-between; + padding: 8px 12px; + border-radius: calc(0.5rem - 0.25rem); + cursor: pointer; + font-size: 14px; +} + +.select-option[data-disabled="true"] { + color: var(--secondary-color-5); + cursor: not-allowed; +} + +.select-option:hover:not([data-disabled="true"]), +.select-option:focus-visible { + background: var(--light, var(--primary-color-4)) + var(--dark, var(--primary-color-7)); + color: var(--secondary-color-1); + outline: none; +} + +.select-group-label { + padding: 4px 12px; + color: var(--secondary-color-5); + font-size: 0.75rem; +} + +[data-disabled="true"] { + cursor: not-allowed; + opacity: 0.5; +} diff --git a/crates/weaver-app/src/components/slider/component.rs b/crates/weaver-app/src/components/slider/component.rs new file mode 100644 index 0000000..70b00f1 --- /dev/null +++ b/crates/weaver-app/src/components/slider/component.rs @@ -0,0 +1,52 @@ +use dioxus::prelude::*; +use dioxus_primitives::slider::{ + self, SliderProps, SliderRangeProps, SliderThumbProps, SliderTrackProps, +}; + +#[component] +pub fn Slider(props: SliderProps) -> Element { + rsx! { + document::Link { rel: "stylesheet", href: asset!("./style.css") } + slider::Slider { + class: "slider", + value: props.value, + default_value: props.default_value, + min: props.min, + max: props.max, + step: props.step, + disabled: props.disabled, + horizontal: props.horizontal, + inverted: props.inverted, + on_value_change: props.on_value_change, + label: props.label, + attributes: props.attributes, + {props.children} + } + } +} + +#[component] +pub fn SliderTrack(props: SliderTrackProps) -> Element { + rsx! { + slider::SliderTrack { class: "slider-track", attributes: props.attributes, {props.children} } + } +} + +#[component] +pub fn SliderRange(props: SliderRangeProps) -> Element { + rsx! { + slider::SliderRange { class: "slider-range", attributes: props.attributes, {props.children} } + } +} + +#[component] +pub fn SliderThumb(props: SliderThumbProps) -> Element { + rsx! { + slider::SliderThumb { + class: "slider-thumb", + index: props.index, + attributes: props.attributes, + {props.children} + } + } +} diff --git a/crates/weaver-app/src/components/slider/mod.rs b/crates/weaver-app/src/components/slider/mod.rs new file mode 100644 index 0000000..9a8ae55 --- /dev/null +++ b/crates/weaver-app/src/components/slider/mod.rs @@ -0,0 +1,2 @@ +mod component; +pub use component::*; \ No newline at end of file diff --git a/crates/weaver-app/src/components/slider/style.css b/crates/weaver-app/src/components/slider/style.css new file mode 100644 index 0000000..e41bdd0 --- /dev/null +++ b/crates/weaver-app/src/components/slider/style.css @@ -0,0 +1,76 @@ +.slider { + position: relative; + display: flex; + width: 200px; + align-items: center; + padding: 0.5rem 0; + touch-action: none; +} + +.slider[data-orientation="vertical"] { + width: auto; + height: 200px; + flex-direction: column; +} + +.slider-track { + position: relative; + height: 0.5rem; + box-sizing: border-box; + flex-grow: 1; + border-radius: 9999px; + background: var(--primary-color-5); +} + +.slider[data-orientation="vertical"] .slider-track { + width: 4px; + height: 100%; +} + +.slider-range { + position: absolute; + height: 100%; + border-radius: 9999px; + background-color: var(--secondary-color-2); +} + +.slider[data-orientation="vertical"] .slider-range { + width: 100%; +} + +.slider-thumb { + all: unset; + position: absolute; + top: 50%; + display: block; + width: 16px; + height: 16px; + border: 1px solid var(--secondary-color-2); + border-radius: 50%; + background-color: var(--primary-color-1); + cursor: pointer; + transform: translate(-50%, -50%); + transition: border-color 150ms; +} + +.slider[data-orientation="vertical"] .slider-thumb { + left: 50%; + transform: translate(-50%, 50%); +} + +.slider-thumb:focus-visible[data-dragging="true"], +.slider-thumb:focus-visible, +.slider-thumb:hover { + box-shadow: 0 0 0 4px + color-mix(in oklab, var(--primary-color-7) 50%, transparent); + transition: box-shadow 150ms; +} + +.slider[data-disabled="true"] { + cursor: not-allowed; + opacity: 0.5; +} + +.slider[data-disabled="true"] .slider-thumb { + cursor: not-allowed; +} diff --git a/crates/weaver-app/src/components/switch/component.rs b/crates/weaver-app/src/components/switch/component.rs new file mode 100644 index 0000000..f63d7ed --- /dev/null +++ b/crates/weaver-app/src/components/switch/component.rs @@ -0,0 +1,28 @@ +use dioxus::prelude::*; +use dioxus_primitives::switch::{self, SwitchProps, SwitchThumbProps}; + +#[component] +pub fn Switch(props: SwitchProps) -> Element { + rsx! { + document::Link { rel: "stylesheet", href: asset!("./style.css") } + switch::Switch { + class: "switch", + checked: props.checked, + default_checked: props.default_checked, + disabled: props.disabled, + required: props.required, + name: props.name, + value: props.value, + on_checked_change: props.on_checked_change, + attributes: props.attributes, + {props.children} + } + } +} + +#[component] +pub fn SwitchThumb(props: SwitchThumbProps) -> Element { + rsx! { + switch::SwitchThumb { class: "switch-thumb", attributes: props.attributes, {props.children} } + } +} diff --git a/crates/weaver-app/src/components/switch/mod.rs b/crates/weaver-app/src/components/switch/mod.rs new file mode 100644 index 0000000..9a8ae55 --- /dev/null +++ b/crates/weaver-app/src/components/switch/mod.rs @@ -0,0 +1,2 @@ +mod component; +pub use component::*; \ No newline at end of file diff --git a/crates/weaver-app/src/components/switch/style.css b/crates/weaver-app/src/components/switch/style.css new file mode 100644 index 0000000..f26e17d --- /dev/null +++ b/crates/weaver-app/src/components/switch/style.css @@ -0,0 +1,43 @@ +.switch-example { + display: flex; + align-items: center; + padding: 20px; + gap: 15px; +} + +.switch { + all: unset; + position: relative; + width: 2rem; + height: 1.15rem; + border-radius: 9999px; + background-color: var(--primary-color-6); + cursor: pointer; + transition: background-color 150ms; +} + +.switch[data-state="checked"] { + background-color: var(--secondary-color-2); +} + +.switch-thumb { + display: block; + width: calc(1.15rem - 2px); + height: calc(1.15rem - 2px); + border-radius: 9999px; + background-color: var(--light, var(--primary-color)) var(--dark, var(--secondary-color-2)); + transform: translateX(1px); + transition: transform 150ms; + will-change: transform; +} + +.switch[data-state="checked"] .switch-thumb { + background-color: var(--light, var(--primary-color)) var(--dark, var(--primary-color-3)); + transform: translateX(calc(2rem - 1px - (1.15rem - 2px))); +} + +/* Only apply disabled styles when data-disabled is "true" */ +.switch[data-disabled="true"] { + cursor: not-allowed; + opacity: 0.5; +} diff --git a/crates/weaver-app/src/components/toggle_group/component.rs b/crates/weaver-app/src/components/toggle_group/component.rs new file mode 100644 index 0000000..b8f8e77 --- /dev/null +++ b/crates/weaver-app/src/components/toggle_group/component.rs @@ -0,0 +1,34 @@ +use dioxus::prelude::*; +use dioxus_primitives::toggle_group::{self, ToggleGroupProps, ToggleItemProps}; + +#[component] +pub fn ToggleGroup(props: ToggleGroupProps) -> Element { + rsx! { + document::Link { rel: "stylesheet", href: asset!("./style.css") } + toggle_group::ToggleGroup { + class: "toggle-group", + default_pressed: props.default_pressed, + pressed: props.pressed, + on_pressed_change: props.on_pressed_change, + disabled: props.disabled, + allow_multiple_pressed: props.allow_multiple_pressed, + horizontal: props.horizontal, + roving_loop: props.roving_loop, + attributes: props.attributes, + {props.children} + } + } +} + +#[component] +pub fn ToggleItem(props: ToggleItemProps) -> Element { + rsx! { + toggle_group::ToggleItem { + class: "toggle-item", + index: props.index, + disabled: props.disabled, + attributes: props.attributes, + {props.children} + } + } +} diff --git a/crates/weaver-app/src/components/toggle_group/mod.rs b/crates/weaver-app/src/components/toggle_group/mod.rs new file mode 100644 index 0000000..9a8ae55 --- /dev/null +++ b/crates/weaver-app/src/components/toggle_group/mod.rs @@ -0,0 +1,2 @@ +mod component; +pub use component::*; \ No newline at end of file diff --git a/crates/weaver-app/src/components/toggle_group/style.css b/crates/weaver-app/src/components/toggle_group/style.css new file mode 100644 index 0000000..e9e2405 --- /dev/null +++ b/crates/weaver-app/src/components/toggle_group/style.css @@ -0,0 +1,59 @@ +.toggle-group { + width: fit-content; +} + +.toggle-item { + min-width: 35px; + padding: 10px; + border: none; + border-radius: 0; + background-color: transparent; + color: var(--secondary-color-4); + font-size: 14px; + outline: none; + transition: background-color 200ms ease, border 200ms ease; +} + +.toggle-group[data-allow-multiple-pressed="true"] + .toggle-item { + border-top: 1px solid var(--primary-color-6); + border-right: 1px solid var(--primary-color-6); + border-bottom: 1px solid var(--primary-color-6); +} + +.toggle-item:hover, +.toggle-item:focus-visible { + background-color: var(--primary-color-4); + cursor: pointer; +} + +.toggle-item[data-state="on"] { + background-color: var(--primary-color-7); + color: var(--secondary-color-1); +} + +.toggle-group[data-allow-multiple-pressed="true"] + .toggle-item[data-state="on"] { + border-top: 1px solid var(--secondary-color-6); + border-right: 1px solid var(--secondary-color-6); + border-bottom: 1px solid var(--secondary-color-6); +} + +.toggle-group[data-allow-multiple-pressed="true"] + .toggle-item:first-child[data-state="on"] { + border: 1px solid var(--secondary-color-6); +} + +.toggle-item:first-child { + border-bottom-left-radius: 0.5rem; + border-top-left-radius: 0.5rem; +} + +.toggle-group[data-allow-multiple-pressed="true"] .toggle-item:first-child { + border: 1px solid var(--primary-color-6); +} + +.toggle-item:last-child { + border-bottom-right-radius: 0.5rem; + border-top-right-radius: 0.5rem; +} diff --git a/crates/weaver-app/src/components/tooltip/component.rs b/crates/weaver-app/src/components/tooltip/component.rs new file mode 100644 index 0000000..a6486d2 --- /dev/null +++ b/crates/weaver-app/src/components/tooltip/component.rs @@ -0,0 +1,44 @@ +use dioxus::prelude::*; +use dioxus_primitives::tooltip::{self, TooltipContentProps, TooltipProps, TooltipTriggerProps}; + +#[component] +pub fn Tooltip(props: TooltipProps) -> Element { + rsx! { + document::Link { rel: "stylesheet", href: asset!("./style.css") } + tooltip::Tooltip { + class: "tooltip", + disabled: props.disabled, + open: props.open, + default_open: props.default_open, + on_open_change: props.on_open_change, + attributes: props.attributes, + {props.children} + } + } +} + +#[component] +pub fn TooltipTrigger(props: TooltipTriggerProps) -> Element { + rsx! { + tooltip::TooltipTrigger { + class: "tooltip-trigger", + id: props.id, + attributes: props.attributes, + {props.children} + } + } +} + +#[component] +pub fn TooltipContent(props: TooltipContentProps) -> Element { + rsx! { + tooltip::TooltipContent { + class: "tooltip-content", + id: props.id, + side: props.side, + align: props.align, + attributes: props.attributes, + {props.children} + } + } +} diff --git a/crates/weaver-app/src/components/tooltip/mod.rs b/crates/weaver-app/src/components/tooltip/mod.rs new file mode 100644 index 0000000..9a8ae55 --- /dev/null +++ b/crates/weaver-app/src/components/tooltip/mod.rs @@ -0,0 +1,2 @@ +mod component; +pub use component::*; \ No newline at end of file diff --git a/crates/weaver-app/src/components/tooltip/style.css b/crates/weaver-app/src/components/tooltip/style.css new file mode 100644 index 0000000..0388ab8 --- /dev/null +++ b/crates/weaver-app/src/components/tooltip/style.css @@ -0,0 +1,150 @@ +/* Tooltip Styles */ +.tooltip { + position: relative; + display: inline-block; +} + +.tooltip-trigger { + display: inline-block; +} + +.tooltip-content { + position: absolute; + z-index: 1000; + max-width: 250px; + padding: 8px 12px; + border-radius: 0.5rem; + animation: tooltip-fade-in 0.2s ease-in-out; + background-color: var(--secondary-color-4); + color: var(--primary-color); + font-size: 14px; + line-height: 1.4; +} + +.tooltip-content::after { + position: absolute; + border-width: 0.25rem; + border-style: solid; + margin-left: -0.25rem; + content: " "; + rotate: 45deg; +} + +/* Positioning based on side */ +.tooltip-content[data-side="top"] { + position: absolute; + bottom: 100%; + left: 50%; + margin-bottom: 8px; + transform: translateX(-50%); +} + +.tooltip-content[data-side="top"]::after { + top: calc(100% - 0.25rem); + left: 50%; + border-color: var(--secondary-color-4); + border-radius: 0 0 0.1rem; +} + +.tooltip-content[data-side="right"] { + position: absolute; + top: 50%; + left: 100%; + margin-left: 8px; + transform: translateY(-50%); +} + +.tooltip-content[data-side="right"]::after { + top: calc(50% - 0.25rem); + left: 0; + border-color: var(--secondary-color-4); + border-radius: 0 0 0 0.1rem; +} + +.tooltip-content[data-side="bottom"] { + position: absolute; + top: 100%; + left: 50%; + margin-top: 8px; + transform: translateX(-50%); +} + +.tooltip-content[data-side="bottom"]::after { + bottom: calc(100% - 0.25rem); + left: 50%; + border-color: var(--secondary-color-4); + border-radius: 0.1rem 0 0; +} + +.tooltip-content[data-side="left"] { + position: absolute; + top: 50%; + right: 100%; + margin-right: 8px; + transform: translateY(-50%); +} + +.tooltip-content[data-side="left"]::after { + top: calc(50% - 0.25rem); + right: -0.25rem; + border-color: var(--secondary-color-4); + border-radius: 0 0.1rem 0 0; +} + +/* Alignment styles for top and bottom */ +.tooltip-content[data-side="top"][data-align="start"], +.tooltip-content[data-side="bottom"][data-align="start"] { + left: 0; + transform: none; +} + +.tooltip-content[data-side="top"][data-align="end"], +.tooltip-content[data-side="bottom"][data-align="end"] { + right: 0; + left: auto; + transform: none; +} + +/* Alignment styles for left and right */ +.tooltip-content[data-side="left"][data-align="start"], +.tooltip-content[data-side="right"][data-align="start"] { + top: 0; + transform: none; +} + +.tooltip-content[data-side="left"][data-align="center"], +.tooltip-content[data-side="right"][data-align="center"] { + top: 50%; + transform: translateY(-50%); +} + +.tooltip-content[data-side="left"][data-align="end"], +.tooltip-content[data-side="right"][data-align="end"] { + top: auto; + bottom: 0; + transform: none; +} + +/* Animation */ +@keyframes tooltip-fade-in { + from { + opacity: 0; + } + + to { + opacity: 1; + } +} + +/* State styles */ +.tooltip[data-disabled="true"] .tooltip-trigger { + cursor: default; +} + +.tooltip-content[data-state="closed"] { + display: none; +} + +.tooltip-content[data-state="open"] { + display: block; +} diff --git a/docs/graph-data.json b/docs/graph-data.json index d0848a8..144aec0 100644 --- a/docs/graph-data.json +++ b/docs/graph-data.json @@ -2430,6 +2430,61 @@ "created_at": "2026-01-07T23:38:17.591489488-05:00", "updated_at": "2026-01-07T23:38:17.591489488-05:00", "metadata_json": "{\"confidence\":80}" + }, + { + "id": 223, + "change_id": "c6bc88c6-ec9b-4bd8-b5fb-19a99b884694", + "node_type": "goal", + "title": "Phase 2: Custom domain hosting for notebooks", + "description": null, + "status": "pending", + "created_at": "2026-01-10T17:49:33.193809025-05:00", + "updated_at": "2026-01-10T17:49:33.193809025-05:00", + "metadata_json": "{\"confidence\":90,\"prompt\":\"User asked: plan phase 2 of subdomain hosting - custom domains with Caddy/Cloudflare TLS, site.standard.* indexing, CNAME encoding\"}" + }, + { + "id": 224, + "change_id": "e9483041-6861-4697-8f22-c32a2b771ce3", + "node_type": "action", + "title": "Wrote phase 2 implementation plan with custom path routing and landing pages", + "description": null, + "status": "pending", + "created_at": "2026-01-10T18:05:27.835814588-05:00", + "updated_at": "2026-01-10T18:05:27.835814588-05:00", + "metadata_json": "{\"confidence\":95}" + }, + { + "id": 225, + "change_id": "7fbf023e-55b9-47d4-81c2-e72d4aed6a68", + "node_type": "goal", + "title": "Notebook creation/modification UI + site.standard.* record creation", + "description": null, + "status": "pending", + "created_at": "2026-01-11T15:03:42.186678843-05:00", + "updated_at": "2026-01-11T15:03:42.186678843-05:00", + "metadata_json": "{\"branch\":\"main\",\"confidence\":80,\"prompt\":\"User: need components for creating notebooks, modifying settings (publishGlobal, theme), and enabling site.standard.publication/document creation for entries. Requires use-index feature.\"}" + }, + { + "id": 226, + "change_id": "e984ddb9-1347-4caf-8f60-acf4e3722c52", + "node_type": "action", + "title": "Wrote notebook creation/settings design doc", + "description": null, + "status": "pending", + "created_at": "2026-01-11T16:10:01.145513905-05:00", + "updated_at": "2026-01-11T16:10:01.145513905-05:00", + "metadata_json": "{\"confidence\":95}" + }, + { + "id": 227, + "change_id": "71f83579-7dd8-4545-a11d-a57390beaf7d", + "node_type": "action", + "title": "Wrote detailed implementation plan for notebook creation/settings", + "description": null, + "status": "pending", + "created_at": "2026-01-11T16:25:55.912466284-05:00", + "updated_at": "2026-01-11T16:25:55.912466284-05:00", + "metadata_json": "{\"confidence\":95}" } ], "edges": [ @@ -4808,6 +4863,39 @@ "weight": 1.0, "rationale": "Identifies next enhancement for editor-js", "created_at": "2026-01-07T23:38:25.207053545-05:00" + }, + { + "id": 218, + "from_node_id": 223, + "to_node_id": 224, + "from_change_id": "c6bc88c6-ec9b-4bd8-b5fb-19a99b884694", + "to_change_id": "e9483041-6861-4697-8f22-c32a2b771ce3", + "edge_type": "leads_to", + "weight": 1.0, + "rationale": "Plan created for this goal", + "created_at": "2026-01-10T18:05:27.958002778-05:00" + }, + { + "id": 219, + "from_node_id": 225, + "to_node_id": 226, + "from_change_id": "7fbf023e-55b9-47d4-81c2-e72d4aed6a68", + "to_change_id": "e984ddb9-1347-4caf-8f60-acf4e3722c52", + "edge_type": "leads_to", + "weight": 1.0, + "rationale": "Design document for notebook creation goal", + "created_at": "2026-01-11T16:10:01.284910370-05:00" + }, + { + "id": 220, + "from_node_id": 226, + "to_node_id": 227, + "from_change_id": "e984ddb9-1347-4caf-8f60-acf4e3722c52", + "to_change_id": "71f83579-7dd8-4545-a11d-a57390beaf7d", + "edge_type": "leads_to", + "weight": 1.0, + "rationale": "Design doc led to implementation plan", + "created_at": "2026-01-11T16:25:56.055454076-05:00" } ] } \ No newline at end of file diff --git a/lexicons/domain/defs.json b/lexicons/domain/defs.json new file mode 100644 index 0000000..2be5665 --- /dev/null +++ b/lexicons/domain/defs.json @@ -0,0 +1,105 @@ +{ + "lexicon": 1, + "id": "sh.weaver.domain.defs", + "defs": { + "documentView": { + "type": "object", + "description": "Hydrated view of a document with re-hydrated content.", + "required": [ + "uri", + "cid", + "did", + "rkey", + "title", + "path", + "record", + "indexedAt" + ], + "properties": { + "cid": { + "type": "string", + "format": "cid" + }, + "did": { + "type": "string", + "format": "did" + }, + "entryIndex": { + "type": "integer" + }, + "entryUri": { + "type": "string", + "format": "at-uri" + }, + "indexedAt": { + "type": "string", + "format": "datetime" + }, + "path": { + "type": "string" + }, + "record": { + "type": "unknown" + }, + "rkey": { + "type": "string" + }, + "title": { + "type": "string" + }, + "uri": { + "type": "string", + "format": "at-uri" + } + } + }, + "publicationView": { + "type": "object", + "description": "Hydrated view of a publication with domain info.", + "required": [ + "uri", + "cid", + "did", + "rkey", + "name", + "domain", + "record", + "indexedAt" + ], + "properties": { + "cid": { + "type": "string", + "format": "cid" + }, + "did": { + "type": "string", + "format": "did" + }, + "domain": { + "type": "string" + }, + "indexedAt": { + "type": "string", + "format": "datetime" + }, + "name": { + "type": "string" + }, + "notebookUri": { + "type": "string", + "format": "at-uri" + }, + "record": { + "type": "unknown" + }, + "rkey": { + "type": "string" + }, + "uri": { + "type": "string", + "format": "at-uri" + } + } + } + } +} \ No newline at end of file diff --git a/lexicons/domain/generateDocument.json b/lexicons/domain/generateDocument.json new file mode 100644 index 0000000..6dd06a5 --- /dev/null +++ b/lexicons/domain/generateDocument.json @@ -0,0 +1,60 @@ +{ + "lexicon": 1, + "id": "sh.weaver.domain.generateDocument", + "defs": { + "main": { + "type": "query", + "description": "Generate a site.standard.document record from a weaver entry. Returns a ready-to-write record with fully hydrated BookEntryView in content.", + "parameters": { + "type": "params", + "required": [ + "entry", + "publication", + "path" + ], + "properties": { + "entry": { + "type": "string", + "description": "AT-URI of the sh.weaver.notebook.entry to convert.", + "format": "at-uri" + }, + "path": { + "type": "string", + "description": "URL path for the document." + }, + "publication": { + "type": "string", + "description": "AT-URI of the site.standard.publication this document belongs to.", + "format": "at-uri" + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "record" + ], + "properties": { + "record": { + "type": "ref", + "ref": "site.standard.document" + } + } + } + }, + "errors": [ + { + "name": "PublicationNotFound" + }, + { + "name": "EntryNotFound" + }, + { + "name": "NotebookNotLinked" + } + ] + } + } +} \ No newline at end of file diff --git a/lexicons/domain/resolveByDomain.json b/lexicons/domain/resolveByDomain.json new file mode 100644 index 0000000..0885e47 --- /dev/null +++ b/lexicons/domain/resolveByDomain.json @@ -0,0 +1,42 @@ +{ + "lexicon": 1, + "id": "sh.weaver.domain.resolveByDomain", + "defs": { + "main": { + "type": "query", + "description": "Resolve a publication by its custom domain.", + "parameters": { + "type": "params", + "required": [ + "domain" + ], + "properties": { + "domain": { + "type": "string", + "description": "The custom domain to resolve (e.g., myblog.com)." + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "publication" + ], + "properties": { + "publication": { + "type": "ref", + "ref": "sh.weaver.domain.defs#publicationView" + } + } + } + }, + "errors": [ + { + "name": "DomainNotFound" + } + ] + } + } +} \ No newline at end of file diff --git a/lexicons/domain/resolveDocument.json b/lexicons/domain/resolveDocument.json new file mode 100644 index 0000000..6a06ee8 --- /dev/null +++ b/lexicons/domain/resolveDocument.json @@ -0,0 +1,51 @@ +{ + "lexicon": 1, + "id": "sh.weaver.domain.resolveDocument", + "defs": { + "main": { + "type": "query", + "description": "Resolve a document by path within a publication. Returns re-hydrated content for weaver-backed documents.", + "parameters": { + "type": "params", + "required": [ + "publication", + "path" + ], + "properties": { + "path": { + "type": "string", + "description": "URL path within the publication (e.g., /posts/my-first-post)." + }, + "publication": { + "type": "string", + "description": "AT-URI of the publication.", + "format": "at-uri" + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "document" + ], + "properties": { + "document": { + "type": "ref", + "ref": "sh.weaver.domain.defs#documentView" + } + } + } + }, + "errors": [ + { + "name": "PublicationNotFound" + }, + { + "name": "DocumentNotFound" + } + ] + } + } +} \ No newline at end of file diff --git a/lexicons/notebook/book.json b/lexicons/notebook/book.json index c9e5b68..a951977 100644 --- a/lexicons/notebook/book.json +++ b/lexicons/notebook/book.json @@ -45,7 +45,7 @@ "rating": { "type": "ref", "ref": "sh.weaver.notebook.defs#contentRating" }, "publishGlobal": { "type": "boolean", - "description": "Notebook opts into accessiblity by path only without identity scoping. Path must be globally unique." + "description": "Notebook opts into accessiblity by path only without identity scoping. Path must be globally unique and a valid subdomain." } } }