diff --git a/docs/layout/guide.mdx b/docs/layout/guide.mdx index 4daf006a2..1bc4a0453 100644 --- a/docs/layout/guide.mdx +++ b/docs/layout/guide.mdx @@ -4,13 +4,17 @@ icon: "table-cells" description: "" --- -# Using the `size` Parameter in Components +# Layout Tools and Techniques + +Preswald offers several tools to help you control and organize your application's layout. + +## Using the `size` Parameter in Components The `size` parameter lets you control how much space a component occupies in a row, enabling you to create dynamic and responsive layouts. --- -## How the `size` Parameter Works +### How the `size` Parameter Works - **Default Behavior**: All components have a default `size=1.0`, taking up the full width of the row. - **Custom Sizes**: You can specify a `size` less than `1.0` to allow multiple components to share the same row. @@ -19,9 +23,9 @@ The `size` parameter lets you control how much space a component occupies in a r --- -## Example: Multiple Components in One Row +### Example: Multiple Components in One Row -Here’s an example of how to use the `size` parameter to arrange components in a row: +Here's an example of how to use the `size` parameter to arrange components in a row: ```python from preswald import slider, button @@ -35,7 +39,35 @@ submit_button = button("Submit", size=0.3) threshold_slider = slider("Threshold", min_val=0.0, max_val=100.0, default=50.0, size=0.7) ``` ---- - - **Flexible Layouts**: Multiple components with smaller sizes can fit side by side in a single row. - **Spacing Management**: Verify the combined sizes of all components in a row add up to `1.0` or less. + +--- + +## Using the `collapsible` Component for Group Organization + +The `collapsible` component helps you organize related UI elements into expandable/collapsible sections, improving the overall structure and readability of your application. + +### Benefits of Using Collapsible Sections + +- **Reduced Visual Clutter**: Hide optional or advanced controls until needed +- **Logical Grouping**: Group related inputs and outputs +- **Progressive Disclosure**: Implement step-by-step workflows +- **Better Mobile Experience**: Improve usability on smaller screens + +### Example: Organizing Components with Collapsible + +```python +from preswald import collapsible, slider, text, table + +# Main data view (expanded by default) +collapsible("Data Overview") +table(my_dataframe) + +# Advanced filters section (collapsed by default) +collapsible("Advanced Filters", open=False) +text("Adjust parameters to filter the data") +slider("Threshold", min_val=0, max_val=100, default=50) +``` + +See the [`collapsible` documentation](/sdk/collapsible) for more details on its usage and parameters. diff --git a/docs/sdk/collapsible.mdx b/docs/sdk/collapsible.mdx new file mode 100644 index 000000000..b4b1da63a --- /dev/null +++ b/docs/sdk/collapsible.mdx @@ -0,0 +1,65 @@ +--- +title: "collapsible" +icon: "chevron-down" +description: "" +--- + +```python +collapsible( + label: str, + open: bool = True, + size: float = 1.0, +) -> None: +``` + +The `collapsible` function creates an expandable/collapsible container that groups related UI components. This helps organize complex interfaces by reducing visual clutter and allowing users to focus on relevant content. + +## Parameters + +- **`label`** _(str)_: The title/header of the collapsible section. +- **`open`** _(bool)_: Whether the section is open (expanded) by default. Defaults to `True`. +- **`size`** _(float)_: _(Optional)_ The width of the component in a row. Defaults to `1.0` (full row). See the [Layout Guide](/layout/guide) for details. + + +Collapsible component + + +## Returns + +- This component doesn't return a value. It's used for layout organization only. + +## Usage Example + +```python +from preswald import collapsible, slider, text + +# Create a collapsible section for advanced filters +collapsible("Advanced Filters", open=False) + +# All components below will be nested in the collapsible container +text("Adjust the parameters below to filter the data.") +slider("Sepal Width", min_val=0, max_val=10, default=5.5) +slider("Sepal Length", min_val=0, max_val=10, default=4.5) + +# Create another section that's open by default +collapsible("Main Visualizations") +text("These are the primary visualizations for your data.") +# Add more components here... +``` + +### Key Features + +1. **Organized Layout**: Group related components to create a cleaner, more structured interface. +2. **Reduced Visual Clutter**: Hide optional or advanced controls that aren't needed immediately. +3. **Improved User Experience**: Create step-by-step workflows or categorize UI elements by function. +4. **Responsive Design**: Helps make dense dashboards more manageable on smaller screens. + +### Why Use `collapsible`? + +The `collapsible` component is essential for building complex applications with many UI elements. By organizing components into expandable/collapsible sections, you can create more intuitive interfaces that guide users through your data application. + +Enhance your layout with the `collapsible` component! 🔽 \ No newline at end of file diff --git a/examples/iris/hello.py b/examples/iris/hello.py index ca3ed935c..1ed590d30 100644 --- a/examples/iris/hello.py +++ b/examples/iris/hello.py @@ -5,6 +5,7 @@ from preswald import ( chat, + collapsible, # fastplotlib, get_df, plotly, @@ -29,6 +30,9 @@ # Load the CSV df = get_df("iris_csv") +# Add collapsible for Sepal visualizations +collapsible("Sepal Visualizations", open=True) + # 1. Scatter plot - Sepal Length vs Sepal Width text( "## Sepal Length vs Sepal Width \n This scatter plot shows the relationship between sepal length and sepal width for different iris species. We can see that Setosa is well-separated from the other two species, while Versicolor and Virginica show some overlap." @@ -68,6 +72,9 @@ fig5.update_layout(template="plotly_white") plotly(fig5) +# Add collapsible for Petal visualizations +collapsible("Petal Visualizations", open=False) + # 4. Violin plot of Sepal Length by Species text( "## Sepal Length Distribution by Species \n The violin plot provides a better understanding of the distribution of sepal lengths within each species. We can see the density of values and how they vary across species." @@ -96,68 +103,8 @@ fig10.update_layout(template="plotly_white") plotly(fig10) -# # 6. Fastplotlib Examples -# -# # Retrieve client_id from component state -# client_id = service.get_component_state("client_id") -# -# sidebar(defaultopen=True) -# text("# Fastplotlib Examples") -# -# # 6.1. Simple Image Plot -# text("## Simple Image Plot") -# fig = fpl.Figure(size=(700, 560), canvas="offscreen") -# fig._client_id = client_id -# fig._label = "Simple Image Plot" -# data = iio.imread("images/logo.png") -# fig[0, 0].add_image(data) -# fastplotlib(fig) -# -# # 6.2. Line Plot -# text("## Line Plot") -# x = np.linspace(-1, 10, 100) -# y = np.sin(x) -# sine = np.column_stack([x, y]) -# fig = fpl.Figure(size=(700, 560), canvas="offscreen") -# fig._client_id = client_id -# fig._label = "Line Plot" -# fig[0, 0].add_line(data=sine, colors="w") -# fastplotlib(fig) -# -# # 6.3. Line Plot with Color Maps -# text("## Line Plot ColorMap") -# fig = fpl.Figure(size=(700, 560), canvas="offscreen") -# fig._client_id = client_id -# fig._label = "Line Plot Color Map" -# xs = np.linspace(-10, 10, 100) -# ys = np.sin(xs) -# sine = np.dstack([xs, ys])[0] -# ys = np.cos(xs) - 5 -# cosine = np.dstack([xs, ys])[0] -# -# sine_graphic = fig[0, 0].add_line( -# data=sine, thickness=10, cmap="plasma", cmap_transform=sine[:, 1] -# ) -# labels = [0] * 25 + [5] * 10 + [1] * 35 + [2] * 30 -# cosine_graphic = fig[0, 0].add_line( -# data=cosine, thickness=10, cmap="tab10", cmap_transform=labels -# ) -# fastplotlib(fig) -# -# # 6.4. Scatter Plot from Iris dataset -# text("## Scatter Plot") -# x = df["sepal.length"].tolist() -# y = df["petal.width"].tolist() -# variety = df["variety"].tolist() -# data = np.column_stack((x, y)) -# color_map = {"Setosa": "yellow", "Versicolor": "cyan", "Virginica": "magenta"} -# colors = [color_map[v] for v in variety] -# -# fig = fpl.Figure(size=(700, 560), canvas="offscreen") -# fig._client_id = client_id -# fig._label = "Scatter Plot" -# fig[0, 0].add_scatter(data=data, sizes=4, colors=colors) -# fastplotlib(fig) +# Add collapsible for data view +collapsible("Dataset View", open=False) # Show the first 10 rows of the dataset text( diff --git a/frontend/components.json b/frontend/components.json index d1b7544df..4da4ee898 100644 --- a/frontend/components.json +++ b/frontend/components.json @@ -18,4 +18,4 @@ "hooks": "@/hooks" }, "iconLibrary": "lucide" -} \ No newline at end of file +} diff --git a/frontend/eslint.config.js b/frontend/eslint.config.js index aa95a80d2..b9bcbcda4 100644 --- a/frontend/eslint.config.js +++ b/frontend/eslint.config.js @@ -1,24 +1,17 @@ -import js from '@eslint/js' -import globals from 'globals' -import react from 'eslint-plugin-react' -import reactHooks from 'eslint-plugin-react-hooks' -import reactRefresh from 'eslint-plugin-react-refresh' -import importPlugin from 'eslint-plugin-import' - +import js from '@eslint/js'; +import importPlugin from 'eslint-plugin-import'; +import react from 'eslint-plugin-react'; +import reactHooks from 'eslint-plugin-react-hooks'; +import reactRefresh from 'eslint-plugin-react-refresh'; +import globals from 'globals'; export default [ { - ignores: [ - 'dist', - 'node_modules', - '*.config.js', - ] + ignores: ['dist', 'node_modules', '*.config.js'], }, { files: ['**/*.{js,jsx}'], - extends: [ - 'prettier' - ], + extends: ['prettier'], languageOptions: { ecmaVersion: 2020, globals: { @@ -51,48 +44,45 @@ export default [ ...react.configs['jsx-runtime'].rules, ...reactHooks.configs.recommended.rules, 'react/jsx-no-target-blank': 'off', - 'react-refresh/only-export-components': [ - 'warn', - { allowConstantExport: true }, + 'react-refresh/only-export-components': ['warn', { allowConstantExport: true }], + 'no-unused-vars': [ + 'error', + { + argsIgnorePattern: '^_', + varsIgnorePattern: '^_', + }, ], - 'no-unused-vars': ['error', { - argsIgnorePattern: '^_', - varsIgnorePattern: '^_', - }], 'no-console': ['warn', { allow: ['warn', 'error'] }], // Disable ESLint's import ordering to let Prettier handle it 'import/order': 'off', ...react.configs['jsx-runtime'].rules, ...reactHooks.configs.recommended.rules, 'react/jsx-no-target-blank': 'off', - 'react-refresh/only-export-components': [ - 'warn', - { allowConstantExport: true }, + 'react-refresh/only-export-components': ['warn', { allowConstantExport: true }], + 'no-unused-vars': [ + 'error', + { + argsIgnorePattern: '^_', + varsIgnorePattern: '^_', + }, ], - 'no-unused-vars': ['error', { - argsIgnorePattern: '^_', - varsIgnorePattern: '^_', - }], 'no-console': ['warn', { allow: ['warn', 'error'] }], - 'import/order': ['error', { - groups: [ - 'builtin', - 'external', - 'internal', - ['parent', 'sibling'], - 'index', - ], - 'newlines-between': 'always', - pathGroups: [ - { pattern: '^react', group: 'external', position: 'before' }, - { pattern: '^@/components/(.*)$', group: 'internal', position: 'before' }, - { pattern: '^@/(.*)$', group: 'internal' }, - ], - alphabetize: { - order: 'asc', - caseInsensitive: true, + 'import/order': [ + 'error', + { + groups: ['builtin', 'external', 'internal', ['parent', 'sibling'], 'index'], + 'newlines-between': 'always', + pathGroups: [ + { pattern: '^react', group: 'external', position: 'before' }, + { pattern: '^@/components/(.*)$', group: 'internal', position: 'before' }, + { pattern: '^@/(.*)$', group: 'internal' }, + ], + alphabetize: { + order: 'asc', + caseInsensitive: true, + }, }, - }], + ], }, }, -] \ No newline at end of file +]; diff --git a/frontend/postcss.config.js b/frontend/postcss.config.js index 2e7af2b7f..2aa7205d4 100644 --- a/frontend/postcss.config.js +++ b/frontend/postcss.config.js @@ -3,4 +3,4 @@ export default { tailwindcss: {}, autoprefixer: {}, }, -} +}; diff --git a/frontend/src/components/DynamicComponents.jsx b/frontend/src/components/DynamicComponents.jsx index 13b59208a..e75d31158 100644 --- a/frontend/src/components/DynamicComponents.jsx +++ b/frontend/src/components/DynamicComponents.jsx @@ -14,6 +14,7 @@ import BigNumberWidget from './widgets/BigNumberWidget'; import ButtonWidget from './widgets/ButtonWidget'; import ChatWidget from './widgets/ChatWidget'; import CheckboxWidget from './widgets/CheckboxWidget'; +import CollapsibleWidget from './widgets/CollapsibleWidget'; import DAGVisualizationWidget from './widgets/DAGVisualizationWidget'; import DataVisualizationWidget from './widgets/DataVisualizationWidget'; import FastplotlibWidget from './widgets/FastplotlibWidget'; @@ -74,6 +75,18 @@ const MemoizedComponent = memo( case 'sidebar': return ; + case 'collapsible': + return ( + + {props.children} + + ); + case 'button': return ( { + const [isOpen, setIsOpen] = React.useState(_open); + + return ( + + +
setIsOpen(!isOpen)} + > +

{_label}

+ +
+ +
{children}
+
+
+
+ ); +}; + +export default CollapsibleWidget; diff --git a/frontend/tailwind.config.js b/frontend/tailwind.config.js index 3dc4d8be2..fc9e19b0f 100644 --- a/frontend/tailwind.config.js +++ b/frontend/tailwind.config.js @@ -1,96 +1,96 @@ /** @type {import('tailwindcss').Config} */ -import typography from "@tailwindcss/typography"; +import typography from '@tailwindcss/typography'; export default { - darkMode: ["class"], - content: [ - "./index.html", // Include index.html - "./src/**/*.{js,jsx}", // Include all JS and JSX files in src - ], - theme: { - extend: { - borderRadius: { - lg: 'var(--radius)', - md: 'calc(var(--radius) - 2px)', - sm: 'calc(var(--radius) - 4px)' - }, - colors: { - background: 'hsl(var(--background))', - foreground: 'hsl(var(--foreground))', - card: { - DEFAULT: 'hsl(var(--card))', - foreground: 'hsl(var(--card-foreground))' - }, - popover: { - DEFAULT: 'hsl(var(--popover))', - foreground: 'hsl(var(--popover-foreground))' - }, - primary: { - DEFAULT: 'hsl(var(--primary))', - foreground: 'hsl(var(--primary-foreground))' - }, - secondary: { - DEFAULT: 'hsl(var(--secondary))', - foreground: 'hsl(var(--secondary-foreground))' - }, - muted: { - DEFAULT: 'hsl(var(--muted))', - foreground: 'hsl(var(--muted-foreground))' - }, - accent: { - DEFAULT: 'hsl(var(--accent))', - foreground: 'hsl(var(--accent-foreground))' - }, - destructive: { - DEFAULT: 'hsl(var(--destructive))', - foreground: 'hsl(var(--destructive-foreground))' - }, - border: 'hsl(var(--border))', - input: 'hsl(var(--input))', - ring: 'hsl(var(--ring))', - chart: { - '1': 'hsl(var(--chart-1))', - '2': 'hsl(var(--chart-2))', - '3': 'hsl(var(--chart-3))', - '4': 'hsl(var(--chart-4))', - '5': 'hsl(var(--chart-5))' - }, - sidebar: { - DEFAULT: 'hsl(var(--sidebar-background))', - foreground: 'hsl(var(--sidebar-foreground))', - primary: 'hsl(var(--sidebar-primary))', - 'primary-foreground': 'hsl(var(--sidebar-primary-foreground))', - accent: 'hsl(var(--sidebar-accent))', - 'accent-foreground': 'hsl(var(--sidebar-accent-foreground))', - border: 'hsl(var(--sidebar-border))', - ring: 'hsl(var(--sidebar-ring))' - } - }, - keyframes: { - "accordion-down": { - from: { height: 0 }, - to: { height: "var(--radix-accordion-content-height)" }, - }, - "accordion-up": { - from: { height: "var(--radix-accordion-content-height)" }, - to: { height: 0 }, - }, - "slide-from-left": { - "0%": { transform: "translateX(-100%)" }, - "100%": { transform: "translateX(0)" }, - }, - "slide-to-left": { - "0%": { transform: "translateX(0)" }, - "100%": { transform: "translateX(-100%)" }, - }, - }, - animation: { - "accordion-down": "accordion-down 0.2s ease-out", - "accordion-up": "accordion-up 0.2s ease-out", - "slide-from-left": "slide-from-left 0.3s ease-out", - "slide-to-left": "slide-to-left 0.3s ease-in", - }, - } - }, - plugins: [typography, require("tailwindcss-animate")], + darkMode: ['class'], + content: [ + './index.html', // Include index.html + './src/**/*.{js,jsx}', // Include all JS and JSX files in src + ], + theme: { + extend: { + borderRadius: { + lg: 'var(--radius)', + md: 'calc(var(--radius) - 2px)', + sm: 'calc(var(--radius) - 4px)', + }, + colors: { + background: 'hsl(var(--background))', + foreground: 'hsl(var(--foreground))', + card: { + DEFAULT: 'hsl(var(--card))', + foreground: 'hsl(var(--card-foreground))', + }, + popover: { + DEFAULT: 'hsl(var(--popover))', + foreground: 'hsl(var(--popover-foreground))', + }, + primary: { + DEFAULT: 'hsl(var(--primary))', + foreground: 'hsl(var(--primary-foreground))', + }, + secondary: { + DEFAULT: 'hsl(var(--secondary))', + foreground: 'hsl(var(--secondary-foreground))', + }, + muted: { + DEFAULT: 'hsl(var(--muted))', + foreground: 'hsl(var(--muted-foreground))', + }, + accent: { + DEFAULT: 'hsl(var(--accent))', + foreground: 'hsl(var(--accent-foreground))', + }, + destructive: { + DEFAULT: 'hsl(var(--destructive))', + foreground: 'hsl(var(--destructive-foreground))', + }, + border: 'hsl(var(--border))', + input: 'hsl(var(--input))', + ring: 'hsl(var(--ring))', + chart: { + 1: 'hsl(var(--chart-1))', + 2: 'hsl(var(--chart-2))', + 3: 'hsl(var(--chart-3))', + 4: 'hsl(var(--chart-4))', + 5: 'hsl(var(--chart-5))', + }, + sidebar: { + DEFAULT: 'hsl(var(--sidebar-background))', + foreground: 'hsl(var(--sidebar-foreground))', + primary: 'hsl(var(--sidebar-primary))', + 'primary-foreground': 'hsl(var(--sidebar-primary-foreground))', + accent: 'hsl(var(--sidebar-accent))', + 'accent-foreground': 'hsl(var(--sidebar-accent-foreground))', + border: 'hsl(var(--sidebar-border))', + ring: 'hsl(var(--sidebar-ring))', + }, + }, + keyframes: { + 'accordion-down': { + from: { height: 0 }, + to: { height: 'var(--radix-accordion-content-height)' }, + }, + 'accordion-up': { + from: { height: 'var(--radix-accordion-content-height)' }, + to: { height: 0 }, + }, + 'slide-from-left': { + '0%': { transform: 'translateX(-100%)' }, + '100%': { transform: 'translateX(0)' }, + }, + 'slide-to-left': { + '0%': { transform: 'translateX(0)' }, + '100%': { transform: 'translateX(-100%)' }, + }, + }, + animation: { + 'accordion-down': 'accordion-down 0.2s ease-out', + 'accordion-up': 'accordion-up 0.2s ease-out', + 'slide-from-left': 'slide-from-left 0.3s ease-out', + 'slide-to-left': 'slide-to-left 0.3s ease-in', + }, + }, + }, + plugins: [typography, require('tailwindcss-animate')], }; diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json index 5e0998e19..53c904d2c 100644 --- a/frontend/tsconfig.json +++ b/frontend/tsconfig.json @@ -1,10 +1,10 @@ { - "include": ["src/**/*.tsx", "src/**/*.ts"], - "compilerOptions": { - "jsx": "react", - "baseUrl": ".", - "paths": { - "@/*": ["./src/*"] - } + "include": ["src/**/*.tsx", "src/**/*.ts"], + "compilerOptions": { + "jsx": "react", + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"] } - } \ No newline at end of file + } +} diff --git a/frontend/vite.config.js b/frontend/vite.config.js index a9f750466..fc1ea4452 100644 --- a/frontend/vite.config.js +++ b/frontend/vite.config.js @@ -1,23 +1,23 @@ -import { defineConfig } from "vite"; -import path from "path"; -import react from "@vitejs/plugin-react"; +import react from '@vitejs/plugin-react'; +import path from 'path'; +import { defineConfig } from 'vite'; // https://vite.dev/config/ export default defineConfig({ build: { - outDir: path.join(path.dirname(__dirname), "preswald", "static"), + outDir: path.join(path.dirname(__dirname), 'preswald', 'static'), emptyOutDir: true, }, - publicDir: "public", + publicDir: 'public', plugins: [react()], resolve: { alias: { - "@": path.resolve(__dirname, "./src"), + '@': path.resolve(__dirname, './src'), }, }, server: { proxy: { - "/api": "http://localhost:8501", // Forward API requests to FastAPI + '/api': 'http://localhost:8501', // Forward API requests to FastAPI }, }, }); diff --git a/preswald/interfaces/__init__.py b/preswald/interfaces/__init__.py index b177c0661..6ac53c937 100644 --- a/preswald/interfaces/__init__.py +++ b/preswald/interfaces/__init__.py @@ -9,6 +9,7 @@ button, chat, checkbox, + collapsible, # fastplotlib, image, json_viewer, diff --git a/preswald/interfaces/components.py b/preswald/interfaces/components.py index 5d9a97354..c6ba3c13c 100644 --- a/preswald/interfaces/components.py +++ b/preswald/interfaces/components.py @@ -1,5 +1,6 @@ # Standard Library import base64 +import hashlib import io import json import logging @@ -203,6 +204,37 @@ def checkbox( return ComponentReturn(current_value, component) +@with_render_tracking("collapsible") +def collapsible( + label: str, open: bool = True, size: float = 1.0, component_id: str | None = None +) -> ComponentReturn: + """Create a collapsible component to group related UI components. + + Args: + label: Title/header of the collapsible section + open: Whether the section is open by default + size: Size of the component relative to container (0.0-1.0) + + Returns: + ComponentReturn: Component metadata + """ + component_id = ( + component_id or f"collapsible-{hashlib.md5(label.encode()).hexdigest()[:8]}" + ) + + # SAFE ID + component = { + "type": "collapsible", + "id": component_id, + "label": label, + "open": open, + "size": size, + } + + logger.debug(f"[collapsible] ID={component_id}, label={label}") + return ComponentReturn(component, component) + + # def fastplotlib(fig: "fplt.Figure", size: float = 1.0) -> str: # """ # Render a Fastplotlib figure and asynchronously stream the resulting image to the frontend. @@ -764,6 +796,12 @@ def spinner( return ComponentReturn(None, component) +@with_render_tracking("sidebar") +def sidebar( + defaultopen: bool = False, component_id: str | None = None +) -> ComponentReturn: + """Create a sidebar component.""" + @with_render_tracking("sidebar") def sidebar( defaultopen: bool = False,