diff --git a/.bazelrc b/.bazelrc
index 22d39f0..329926a 100644
--- a/.bazelrc
+++ b/.bazelrc
@@ -81,5 +81,11 @@ build --experimental_reuse_sandbox_directories
# Valdi Open Source Flags
build --define=open_source_build=true
+build --flag_alias=valdi_js_engine=@valdi//bzl/valdi:js_engine
-build --android_crosstool_top="@snap_client_toolchains//:android_crosstool"
\ No newline at end of file
+# Enable web compilation (generates JS outputs from Valdi compiler for web targets)
+common --define=enable_web=true
+
+build --android_crosstool_top="@snap_client_toolchains//:android_crosstool"
+# Web build configuration (used by scripts/bazel_web_serve.sh)
+build:web --build_tag_filters=web
diff --git a/.cursor/rules/README.md b/.cursor/rules/README.md
deleted file mode 100644
index c802020..0000000
--- a/.cursor/rules/README.md
+++ /dev/null
@@ -1,24 +0,0 @@
-# Cursor Rules for Valdi Widgets
-
-Context-specific rules for AI assistants in this repo. Valdi Widgets is TypeScript/TSX + Bazel; it depends on [Valdi](https://github.com/Snapchat/Valdi) via `WORKSPACE`.
-
-## How Cursor Rules Work
-
-Rules load based on what you're editing:
-
-- `valdi_modules/**/*.ts`, `valdi_modules/**/*.tsx` → **typescript-tsx.md**
-- `**/BUILD.bazel`, `**/*.bzl`, `WORKSPACE` → **bazel.md**
-- `**/test/**/*.ts`, `**/*.spec.ts` → **testing.md**
-
-## Rules
-
-| File | Applies To | Description |
-|------|-----------|-------------|
-| `typescript-tsx.md` | `valdi_modules/**/*.ts`, `valdi_modules/**/*.tsx` | Valdi component patterns, anti-React warnings |
-| `bazel.md` | `**/BUILD.bazel`, `**/*.bzl`, `WORKSPACE` | Bazel conventions |
-| `testing.md` | `**/test/**/*.ts`, `**/*.spec.ts` | Testing (Jasmine) |
-
-## More
-
-- Valdi: https://github.com/Snapchat/Valdi
-- Repo README: `/README.md`
diff --git a/.cursor/rules/bazel.md b/.cursor/rules/bazel.md
deleted file mode 100644
index dafd9b8..0000000
--- a/.cursor/rules/bazel.md
+++ /dev/null
@@ -1,54 +0,0 @@
-# Bazel Build System Rules
-
-**Applies to**: `BUILD.bazel`, `*.bzl` files, `WORKSPACE`
-
-## Overview
-
-Valdi Widgets uses Bazel as its build system, with Valdi as an external dependency (`@valdi//...`).
-
-## Key Commands
-
-```bash
-# Build macOS app
-bazel build //valdi_modules/playground:app_macos
-
-# Run tests
-bazel test //valdi_modules/widgets:test //valdi_modules/navigation:test //valdi_modules/valdi_standalone_ui:test
-
-# Build specific module
-bazel build //valdi_modules/widgets:widgets
-```
-
-## Valdi Dependency
-
-- Valdi is loaded via `http_archive` in WORKSPACE (release tag, e.g. `beta-0.0.2`).
-- Valdi build rules live in `@valdi//bzl/valdi/`.
-- Custom rules: `valdi_module`, `valdi_application` (from Valdi).
-
-## Conventions
-
-### File Naming
-
-- `BUILD.bazel` not `BUILD` (explicit extension)
-- `.bzl` for Starlark macros and rules
-
-### Targets
-
-- Use descriptive target names
-- One main target per BUILD file usually matches directory name
-
-### Dependencies
-
-- Be explicit about dependencies
-- Use `@valdi//...` for Valdi module deps (e.g. `@valdi//src/valdi_modules/src/valdi/valdi_core`)
-- Use visibility to control access
-
-## Configuration
-
-- `.bazelrc` – Build flags and configurations
-- `WORKSPACE` – Workspace and repository configuration
-
-## More Information
-
-- Bazel docs: https://bazel.build
-- Valdi: https://github.com/Snapchat/Valdi
diff --git a/.cursor/rules/testing.md b/.cursor/rules/testing.md
deleted file mode 100644
index 95af340..0000000
--- a/.cursor/rules/testing.md
+++ /dev/null
@@ -1,58 +0,0 @@
-# Testing Rules
-
-**Applies to**: Test files in `**/test/`, `**/*.spec.ts`, `**/*.test.ts`
-
-## Overview
-
-Valdi Widgets uses the same testing approach as Valdi: Jasmine for TypeScript/component tests.
-
-## Test Framework
-
-### Jasmine for TypeScript Tests
-
-```typescript
-import 'jasmine/src/jasmine';
-import { Component } from 'valdi_core/src/Component';
-
-describe('MyComponent', () => {
- it('should render correctly', () => {
- const component = new MyComponent();
- expect(component).toBeDefined();
- });
-
- it('should handle state updates', () => {
- const component = new MyStatefulComponent();
- component.setState({ count: 1 });
- expect(component.state.count).toBe(1);
- });
-});
-```
-
-Test files use `.spec.ts` and live under `test/` in each module.
-
-## Running Tests
-
-```bash
-# Run Valdi Widgets tests
-bazel test //valdi_modules/widgets:test //valdi_modules/navigation:test //valdi_modules/valdi_standalone_ui:test //valdi_modules/navigation_internal:test
-
-# With output
-bazel test //valdi_modules/...:test --test_output=errors
-```
-
-## Test Conventions
-
-- `*.spec.ts` or `*.test.ts` for unit tests
-- `test/` directory per module
-- Test file should mirror source file name
-
-## Important
-
-1. **Test behavior, not implementation**
-2. **Isolate tests** – Each test independent
-3. **Mock dependencies** when appropriate
-4. **Use this.setTimeoutDisposable()** in component code; avoid raw setTimeout/setInterval in tests when testing components
-
-## More Information
-
-- Valdi testing: https://github.com/Snapchat/Valdi
diff --git a/.cursor/rules/typescript-tsx.md b/.cursor/rules/typescript-tsx.md
deleted file mode 100644
index 4b84496..0000000
--- a/.cursor/rules/typescript-tsx.md
+++ /dev/null
@@ -1,106 +0,0 @@
-# Valdi TypeScript/TSX Component Rules
-
-**Applies to**: TypeScript and TSX files in `valdi_modules/**/*.ts`, `valdi_modules/**/*.tsx`
-
-## 🚨 CRITICAL: Valdi is NOT React!
-
-**AI assistants frequently suggest React patterns that DON'T EXIST in Valdi.** Despite using TSX/JSX syntax, Valdi compiles to native code.
-
-### Most Common Mistakes
-
-```typescript
-// ❌ NEVER use React hooks (don't exist!)
-const [count, setCount] = useState(0); // ❌
-useEffect(() => { ... }, []); // ❌
-
-// ❌ NEVER use functional components (don't exist!)
-const MyComponent = () => ; // ❌
-
-// ❌ Common hallucinations
-this.props.title; // Should be: this.viewModel.title
-this.markNeedsRender(); // Doesn't exist! Use setState()
-onMount() { } // Should be: onCreate()
-return ; // onRender() returns void!
-```
-
-### ✅ Correct Valdi Patterns
-
-```typescript
-import { StatefulComponent } from 'valdi_core/src/Component';
-
-class MyComponent extends StatefulComponent {
- state = { count: 0 };
-
- onCreate() { } // Component created
- onViewModelUpdate(prev: ViewModel) { } // Props changed
- onDestroy() { } // Before removal
-
- handleClick = () => {
- this.setState({ count: this.state.count + 1 }); // Auto re-renders
- };
-
- onRender() { // Returns void, not JSX!
- ;
- }
-}
-```
-
-## Quick Reference
-
-| What | React | Valdi |
-|------|-------|-------|
-| **Component** | Function or class | Class only (Component or StatefulComponent) |
-| **State** | `useState(0)` | `state = { count: 0 }` + `setState()` |
-| **Props** | `this.props.title` | `this.viewModel.title` |
-| **Mount** | `useEffect(() => {}, [])` | `onCreate()` |
-| **Update** | `useEffect(() => {}, [dep])` | `onViewModelUpdate(prev)` |
-| **Unmount** | `useEffect(() => () => {}, [])` | `onDestroy()` |
-| **Re-render** | `setCount(...)` | `this.setState(...)` |
-| **Return** | `return ` | `;` (statement) |
-
-## Timers and Scheduling
-
-```typescript
-// ✅ CORRECT - Use component's setTimeoutDisposable
-class MyComponent extends StatefulComponent {
- onCreate() {
- this.setTimeoutDisposable(() => {
- console.log('Delayed action');
- }, 1000);
- }
-
- private scheduleLoop() {
- this.setTimeoutDisposable(() => {
- this.doSomething();
- this.scheduleLoop();
- }, 100);
- }
-}
-
-// ❌ WRONG - Don't use setInterval/setTimeout directly
-setInterval(() => { ... }, 100); // Won't auto-cleanup!
-setTimeout(() => { ... }, 100); // Won't auto-cleanup!
-```
-
-## Common Mistakes to Avoid
-
-1. **Returning JSX from onRender()** – It returns void; JSX is a statement.
-2. **Using this.props** – Use `this.viewModel`.
-3. **Wrong lifecycle** – Use `onCreate` / `onViewModelUpdate` / `onDestroy`.
-4. **Using setInterval/setTimeout directly** – Use `this.setTimeoutDisposable()`.
-
-## Imports
-
-```typescript
-// ✅ CORRECT
-import { Component, StatefulComponent } from 'valdi_core/src/Component';
-import { Style } from 'valdi_core/src/Style';
-
-// ❌ WRONG - React imports don't exist
-import React from 'react';
-import { useState } from 'react';
-```
-
-## More Information
-
-- Valdi: https://github.com/Snapchat/Valdi
diff --git a/.gitignore b/.gitignore
index ffa66ac..f0fb03c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,3 +2,7 @@
bazel-*
.cursor/rules/remote
+
+# Web playground dev server dependencies
+valdi_modules/playground/web_app/node_modules/
+valdi_modules/playground/web_app/package-lock.json
diff --git a/AGENTS.md b/AGENTS.md
index 86f236d..77a7824 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -11,7 +11,7 @@ Valdi Widgets provides reusable UI widgets, styles, and patterns for apps built
- **`valdi_modules/`** – Valdi modules (widgets, navigation, valdi_standalone_ui, navigation_internal, playground)
- **`WORKSPACE`** – Depends on Valdi via `http_archive` (e.g. `beta-0.0.2`)
- **Build**: Bazel; tests: `bazel test //valdi_modules/widgets:test //valdi_modules/navigation:test //valdi_modules/valdi_standalone_ui:test //valdi_modules/navigation_internal:test //valdi_modules/playground:test`
-- **Docs**: `AGENTS.md` (this file), `.cursor/rules/` (typescript-tsx, bazel, testing), `README.md`
+- **Docs**: `AGENTS.md` (this file), `README.md`
The rest of this guide describes **Valdi** patterns so AI assistants don’t suggest React or wrong APIs when editing TypeScript/TSX in `valdi_modules/`.
@@ -107,7 +107,6 @@ const ChildWithProvider = withProviders(MyServiceProvider)(ChildComponent);
- **`valdi_modules/navigation_internal/`** – Internal navigation support
- **`valdi_modules/valdi_standalone_ui/`** – Standalone UI module
- **`valdi_modules/playground/`** – Example app and entry point
-- **`.cursor/rules/`** – Cursor rules (bazel, typescript-tsx, testing, etc.)
Valdi itself (compiler, runtime, core modules) is in the `@valdi//` external repository; see Valdi’s [AGENTS.md](https://github.com/Snapchat/Valdi/blob/main/AGENTS.md) and [docs](https://github.com/Snapchat/Valdi/tree/main/docs) for full framework documentation.
@@ -141,9 +140,27 @@ bazel build //valdi_modules/playground:app_macos
## More Information
-- Valdi: https://github.com/Snapchat/Valdi
-- Valdi Widgets README: `/README.md`
-- Cursor rules: `/.cursor/rules/README.md`
+- Valdi: https://github.com/Snapchat/Valdi
+- Valdi Widgets README: `/README.md`
+
+## AI Assistant Setup
+
+### Install the Valdi CLI
+
+```bash
+npm install -g @snap/valdi
+```
+
+### Install AI skills
+
+Valdi-specific skills for AI coding assistants (component patterns, Bazel conventions, etc.) are bundled in the CLI:
+
+```bash
+valdi skills install # all detected AI tools
+valdi skills install --for=claude # Claude Code only
+valdi skills install --category=client # module-development skills only
+valdi skills list # see all available skills and install status
+```
---
diff --git a/WORKSPACE b/WORKSPACE
index 9af652b..b300988 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -3,15 +3,22 @@ workspace(name = "valdi_widgets")
load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")
load("@bazel_tools//tools/build_defs/repo:git.bzl", "git_repository")
-# Valdi release beta-0.0.2 (https://github.com/Snapchat/Valdi/releases)
+# Valdi bleeding edge (https://github.com/Snapchat/Valdi)
http_archive(
name = "valdi",
- strip_prefix = "Valdi-beta-0.0.2",
- url = "https://github.com/Snapchat/Valdi/archive/refs/tags/beta-0.0.2.tar.gz",
+ strip_prefix = "Valdi-45b28375a189da8f9c24544e3b2d40318d3386a9",
+ url = "https://github.com/Snapchat/Valdi/archive/45b28375a189da8f9c24544e3b2d40318d3386a9.tar.gz",
)
-# For local development:
-# local_repository(name = "valdi", path = "/path/to/Valdi")
+# For local development (uncomment to use local Valdi checkout):
+# local_repository(name = "valdi", path = "/Users/cholgate/Snapchat/Dev/mobile/client/src/open_source")
+
+# Valdi release beta-0.0.2 (https://github.com/Snapchat/Valdi/releases)
+# http_archive(
+# name = "valdi",
+# strip_prefix = "Valdi-beta-0.0.2",
+# url = "https://github.com/Snapchat/Valdi/archive/refs/tags/beta-0.0.2.tar.gz",
+# )
load("@valdi//bzl:workspace_prepare.bzl", "valdi_prepare_workspace")
diff --git a/scripts/bazel_web_serve.sh b/scripts/bazel_web_serve.sh
new file mode 100755
index 0000000..c0d5c55
--- /dev/null
+++ b/scripts/bazel_web_serve.sh
@@ -0,0 +1,49 @@
+#!/usr/bin/env bash
+# Build the playground web bundle and run the **webpack dev server** (Playground app in the browser).
+# This does NOT serve a raw file list — it runs webpack-dev-server so http://localhost:8080 shows the app.
+# Requires: Node.js, npm.
+
+set -euo pipefail
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null && pwd)"
+ROOT_DIR="$SCRIPT_DIR/../"
+WEB_TARGET="//valdi_modules/playground:playground_export_npm"
+WEB_APP_DIR="$ROOT_DIR/valdi_modules/playground/web_app"
+PORT="${PORT:-8080}"
+
+pushd "$ROOT_DIR"
+
+# If something is already on the port (e.g. old python static server), warn
+if command -v lsof &>/dev/null && lsof -i ":$PORT" -sTCP:LISTEN -t &>/dev/null; then
+ echo "Warning: port $PORT is in use. Stop the other process (e.g. a previous file server) so webpack-dev-server can use it."
+ echo " Example: lsof -i :$PORT -sTCP:LISTEN -t | xargs kill"
+ exit 1
+fi
+
+echo "Building web bundle..."
+bazel build "$WEB_TARGET"
+
+OUT_DIR="$ROOT_DIR/bazel-bin/valdi_modules/playground/playground_export_npm"
+if [[ ! -d "$OUT_DIR" ]]; then
+ OUT_DIR=$(bazel info bazel-bin 2>/dev/null)/valdi_modules/playground/playground_export_npm
+fi
+if [[ ! -d "$OUT_DIR" ]]; then
+ echo "Error: could not find built output"
+ exit 1
+fi
+
+export PLAYGROUND_NPM_PATH="$OUT_DIR"
+export PORT="$PORT"
+
+if [[ ! -d "$WEB_APP_DIR/node_modules" ]]; then
+ echo "Installing web_app dependencies (npm install)..."
+ (cd "$WEB_APP_DIR" && npm install --registry https://registry.npmjs.org/)
+fi
+
+echo "Starting webpack-dev-server (Playground app) at http://localhost:$PORT"
+cd "$WEB_APP_DIR"
+if [[ -x ./node_modules/.bin/webpack ]]; then
+ exec ./node_modules/.bin/webpack serve --mode development
+else
+ exec npx --yes webpack serve --mode development
+fi
diff --git a/valdi_modules/playground/BUILD.bazel b/valdi_modules/playground/BUILD.bazel
index 0120850..f45f15c 100644
--- a/valdi_modules/playground/BUILD.bazel
+++ b/valdi_modules/playground/BUILD.bazel
@@ -1,5 +1,6 @@
-load("@valdi//bzl/valdi:valdi_module.bzl", "valdi_module")
load("@valdi//bzl/valdi:valdi_application.bzl", "valdi_application")
+load("@valdi//bzl/valdi:valdi_exported_library.bzl", "valdi_exported_library")
+load("@valdi//bzl/valdi:valdi_module.bzl", "valdi_module")
valdi_application(
name = "app",
@@ -33,3 +34,14 @@ valdi_module(
"@valdi//src/valdi_modules/src/valdi/valdi_tsx",
],
)
+
+valdi_exported_library(
+ name = "playground_export",
+ ios_bundle_id = "com.snap.valdi.playground",
+ ios_bundle_name = "ValdiPlayground",
+ web_package_name = "playground_export_npm",
+ deps = [
+ ":playground",
+ "@valdi//src/valdi_modules/src/valdi/web_renderer",
+ ],
+)
diff --git a/valdi_modules/playground/src/Playground.tsx b/valdi_modules/playground/src/Playground.tsx
index 50d35f1..6de8ee6 100644
--- a/valdi_modules/playground/src/Playground.tsx
+++ b/valdi_modules/playground/src/Playground.tsx
@@ -19,6 +19,7 @@ import { TabsHeader } from 'widgets/src/components/tabs/TabsHeader'
import { TabsCoordinator} from 'widgets/src/components/tabs/TabsCoordinator'
import { convertToTabItems} from 'widgets/src/components/tabs/TabsItemWithTitle'
import { ScrollViewHandler } from 'widgets/src/components/scroll/ScrollViewHandler';
+import { WidgetsCatalog } from './WidgetsCatalog';
declare const runtime: ValdiRuntime;
@@ -36,7 +37,9 @@ export interface PlaygroundContext {
* Internal state of the component.
*/
interface PlaygroundState {
- theme: string
+ theme: string;
+ showCatalog: boolean;
+ catalogReady: boolean;
}
/**
* @Component
@@ -47,7 +50,9 @@ interface PlaygroundState {
*/
export class Playground extends StatefulComponent<{}, PlaygroundState, PlaygroundContext> {
state: PlaygroundState = {
- theme: ThemeLight
+ theme: ThemeLight,
+ showCatalog: true,
+ catalogReady: true,
};
onCreate(): void {
@@ -89,6 +94,28 @@ export class Playground extends StatefulComponent<{}, PlaygroundState, Playgroun
onRender() {
{ setTheme(this.state.theme, false) }
+ if (this.state.showCatalog) {
+
+
+ this.setState({ showCatalog: false, catalogReady: false })}
+ />
+
+
+ {this.state.catalogReady && (
+
+
+
+ )}
+ ;
+ if (!this.state.catalogReady) {
+ this.setTimeoutDisposable(() => this.setState({ catalogReady: true }), 0);
+ }
+ return;
+ }
@@ -162,6 +189,15 @@ export class Playground extends StatefulComponent<{}, PlaygroundState, Playgroun
})}
+
+ this.setState({ showCatalog: true, catalogReady: false })}
+ />
+
+
{
+ state: CatalogState = {
+ date: new Date(),
+ time: { hourOfDay: new Date().getHours(), minuteOfHour: 0 },
+ fruitIndex: 0,
+ };
+
+ onRender(): void {
+ const { date, time, fruitIndex } = this.state;
+ const dateStr = date.toLocaleDateString();
+ const timeStr = `${String(time.hourOfDay).padStart(2, '0')}:${String(time.minuteOfHour).padStart(2, '0')}`;
+ const fruitLabel = FRUIT_LABELS[fruitIndex];
+
+
+
+
+ this.setState({ date: e.date })}
+ />
+
+
+
+
+
+
+ this.setState({ time: e.pickedTime })}
+ />
+
+
+
+
+
+
+ this.setState({ fruitIndex: i })}
+ />
+
+
+
+
+
+
+
+ ;
+ }
+}
diff --git a/valdi_modules/playground/web_app/package.json b/valdi_modules/playground/web_app/package.json
new file mode 100644
index 0000000..ad97ab8
--- /dev/null
+++ b/valdi_modules/playground/web_app/package.json
@@ -0,0 +1,24 @@
+{
+ "name": "playground-web-app",
+ "version": "1.0.0",
+ "private": true,
+ "scripts": {
+ "serve": "webpack serve --mode development",
+ "build": "webpack --mode production"
+ },
+ "dependencies": {
+ "path-browserify": "^1.0.1",
+ "react": "^17.0.2",
+ "react-dom": "^17.0.2"
+ },
+ "devDependencies": {
+ "@babel/core": "^7.23.0",
+ "@babel/preset-env": "^7.23.0",
+ "@babel/preset-react": "^7.23.0",
+ "babel-loader": "^9.1.0",
+ "html-webpack-plugin": "^5.5.0",
+ "webpack": "^5.89.0",
+ "webpack-cli": "^5.1.0",
+ "webpack-dev-server": "^4.15.0"
+ }
+}
diff --git a/valdi_modules/playground/web_app/src/App.js b/valdi_modules/playground/web_app/src/App.js
new file mode 100644
index 0000000..a9c2b11
--- /dev/null
+++ b/valdi_modules/playground/web_app/src/App.js
@@ -0,0 +1,35 @@
+import React from 'react';
+
+// Must be the first Valdi import (sets up runtime)
+import { ValdiWebRenderer } from 'playground_export_npm/src/web_renderer/src/ValdiWebRenderer';
+
+// Register native modules for web
+require('playground_export_npm/src/RegisterNativeModules');
+
+import { PlaygroundApp } from 'playground_export_npm/src/playground/src/PlaygroundApp';
+
+class PlaygroundDemo extends React.Component {
+ constructor(props) {
+ super(props);
+ this.hostRef = React.createRef();
+ }
+
+ componentDidMount() {
+ if (this.hostRef.current) {
+ const webRenderer = new ValdiWebRenderer(this.hostRef.current);
+ webRenderer.renderRootComponent(PlaygroundApp, {}, {}, {});
+ }
+ }
+
+ render() {
+ return (
+
+ );
+ }
+}
+
+const App = () => ;
+
+export default App;
diff --git a/valdi_modules/playground/web_app/src/index.html b/valdi_modules/playground/web_app/src/index.html
new file mode 100644
index 0000000..53f2563
--- /dev/null
+++ b/valdi_modules/playground/web_app/src/index.html
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+ Valdi Playground - React with Webpack
+
+
+
+
+
+
+
diff --git a/valdi_modules/playground/web_app/src/index.js b/valdi_modules/playground/web_app/src/index.js
new file mode 100644
index 0000000..91c8142
--- /dev/null
+++ b/valdi_modules/playground/web_app/src/index.js
@@ -0,0 +1,6 @@
+import React from 'react';
+import ReactDOM from 'react-dom';
+import App from './App';
+
+const el = document.getElementById('app');
+ReactDOM.render(, el);
diff --git a/valdi_modules/playground/web_app/webpack.config.js b/valdi_modules/playground/web_app/webpack.config.js
new file mode 100644
index 0000000..1137f19
--- /dev/null
+++ b/valdi_modules/playground/web_app/webpack.config.js
@@ -0,0 +1,56 @@
+const path = require('path');
+const HtmlWebpackPlugin = require('html-webpack-plugin');
+
+// Path to the Bazel-built playground_export_npm (set by bazel_web_serve.sh or default relative to repo root).
+const REPO_ROOT = path.resolve(__dirname, '../../..');
+const PLAYGROUND_NPM = process.env.PLAYGROUND_NPM_PATH ||
+ path.resolve(REPO_ROOT, 'bazel-bin/valdi_modules/playground/playground_export_npm');
+
+module.exports = {
+ entry: './src/index.js',
+ output: {
+ filename: 'bundle.js',
+ path: path.resolve(__dirname, 'dist'),
+ clean: true,
+ },
+ mode: 'development',
+ devtool: 'source-map',
+ ignoreWarnings: [{ message: /Cannot statically analyse 'require/ }],
+ plugins: [
+ new HtmlWebpackPlugin({ template: 'src/index.html' }),
+ new (require('webpack').IgnorePlugin)({
+ resourceRegExp: /foundation[\\/]test[\\/]util[\\/]lib[\\/]faker\.js$/,
+ }),
+ ],
+ devServer: {
+ port: parseInt(process.env.PORT || '8080', 10),
+ hot: true,
+ static: false,
+ },
+ resolve: {
+ extensions: ['.js', '.jsx', '.ts', '.tsx'],
+ modules: [path.resolve(__dirname, 'node_modules'), 'node_modules'],
+ alias: {
+ 'playground_export_npm': PLAYGROUND_NPM,
+ },
+ },
+ module: {
+ rules: [
+ {
+ test: /\.jsx?$/,
+ exclude: /node_modules/,
+ use: {
+ loader: 'babel-loader',
+ options: {
+ presets: [
+ ['@babel/preset-env', { targets: { esmodules: true } }],
+ ['@babel/preset-react', { runtime: 'automatic' }],
+ ],
+ },
+ },
+ },
+ { test: /\.(png|woff|woff2|eot|ttf|svg)$/, type: 'asset/resource' },
+ { test: /\.protodecl$/, type: 'asset/resource' },
+ ],
+ },
+};
diff --git a/valdi_modules/widgets/BUILD.bazel b/valdi_modules/widgets/BUILD.bazel
index 954bcb5..b355c2b 100644
--- a/valdi_modules/widgets/BUILD.bazel
+++ b/valdi_modules/widgets/BUILD.bazel
@@ -1,4 +1,44 @@
+load("@aspect_rules_ts//ts:defs.bzl", "ts_project")
load("@valdi//bzl/valdi:valdi_module.bzl", "valdi_module")
+load("@valdi//bzl/valdi:valdi_android_library.bzl", "valdi_android_library")
+
+# Web polyglot implementations (DatePicker, TimePicker, IndexPicker, EmojiLabel).
+# The build system's generate_register_native_modules picks up the compiled JS
+# and checks for webPolyglotViews exports to register custom view factories.
+ts_project(
+ name = "widgets_web",
+ srcs = ["web/src/WidgetsWeb.ts"],
+ transpiler = "tsc",
+ tsconfig = "web/tsconfig.json",
+ visibility = ["//visibility:public"],
+)
+
+# Android attribute binders for polyglot custom views (IndexPicker, DatePicker, TimePicker).
+valdi_android_library(
+ name = "widgets_android",
+ srcs = glob(["android/*.kt"]),
+ deps = ["@valdi//valdi:valdi_java"],
+)
+
+# iOS native views for polyglot custom views (IndexPicker, DatePicker, TimePicker, EmojiLabel).
+objc_library(
+ name = "widgets_ios_impl",
+ srcs = glob(["ios/**/*.m"]),
+ hdrs = glob(["ios/**/*.h"]),
+ sdk_frameworks = ["UIKit"],
+ deps = [
+ "@valdi//valdi_core:valdi_core_objc",
+ ],
+)
+
+# macOS native views for polyglot custom views (DatePicker, TimePicker, IndexPicker, EmojiLabel).
+objc_library(
+ name = "widgets_macos_impl",
+ srcs = glob(["macos/**/*.m"]),
+ hdrs = glob(["macos/**/*.h"]),
+ sdk_frameworks = ["Cocoa"],
+ deps = ["@valdi//valdi:valdi_macos"],
+)
valdi_module(
name = "widgets",
@@ -10,8 +50,11 @@ valdi_module(
]) + [
"tsconfig.json",
],
+ android_deps = [":widgets_android"],
android_output_target = "release",
ids_yaml = "ids.yaml",
+ ios_deps = [":widgets_ios_impl"],
+ macos_deps = [":widgets_macos_impl"],
ios_module_name = "SCCatalog",
ios_output_target = "release",
res = glob([
@@ -23,6 +66,7 @@ valdi_module(
"res/**/*.webp",
]),
visibility = ["//visibility:public"],
+ web_deps = [":widgets_web"],
deps = [
"@valdi//src/valdi_modules/src/valdi/bridge_observables",
"@valdi//src/valdi_modules/src/valdi/coreutils",
diff --git a/valdi_modules/widgets/android/ValdiDatePickerAttributesBinder.kt b/valdi_modules/widgets/android/ValdiDatePickerAttributesBinder.kt
new file mode 100644
index 0000000..9d051f1
--- /dev/null
+++ b/valdi_modules/widgets/android/ValdiDatePickerAttributesBinder.kt
@@ -0,0 +1,85 @@
+package com.snap.widgets.pickers
+
+import android.content.Context
+import android.view.ViewGroup
+import com.snap.valdi.attributes.AttributesBinder
+import com.snap.valdi.attributes.AttributesBindingContext
+import com.snap.valdi.attributes.RegisterAttributesBinder
+import com.snap.valdi.attributes.impl.animations.ValdiAnimator
+import com.snap.valdi.callable.ValdiFunction
+import com.snap.valdi.views.ValdiDatePicker
+
+@RegisterAttributesBinder
+class ValdiDatePickerAttributesBinder(private val context: Context) : AttributesBinder {
+
+ override val viewClass: Class
+ get() = ValdiDatePicker::class.java
+
+ fun applyDateSeconds(view: ValdiDatePicker, value: Float, animator: ValdiAnimator?) {
+ view.dateSeconds = value
+ }
+
+ fun resetDateSeconds(view: ValdiDatePicker, animator: ValdiAnimator?) {
+ view.dateSeconds = null
+ }
+
+ fun applyMinimumDateSeconds(view: ValdiDatePicker, value: Float, animator: ValdiAnimator?) {
+ view.minimumDateSeconds = value
+ }
+
+ fun resetMinimumDateSeconds(view: ValdiDatePicker, animator: ValdiAnimator?) {
+ view.minimumDateSeconds = null
+ }
+
+ fun applyMaximumDateSeconds(view: ValdiDatePicker, value: Float, animator: ValdiAnimator?) {
+ view.maximumDateSeconds = value
+ }
+
+ fun resetMaximumDateSeconds(view: ValdiDatePicker, animator: ValdiAnimator?) {
+ view.maximumDateSeconds = null
+ }
+
+ private fun applyOnChange(view: ValdiDatePicker, fn: ValdiFunction) {
+ view.onChangeFunction = fn
+ }
+
+ private fun resetOnChange(view: ValdiDatePicker) {
+ view.onChangeFunction = null
+ }
+
+ private fun applyPreferredStyle(view: ValdiDatePicker, value: Int, animator: ValdiAnimator?) {
+ view.preferredStyle = value
+ }
+
+ private fun resetPreferredStyle(view: ValdiDatePicker, animator: ValdiAnimator?) {
+ view.preferredStyle = 1 // spinner
+ }
+
+ private fun noopApplyColor(view: ValdiDatePicker, value: Int, animator: ValdiAnimator?) {
+ // noop
+ // DO NOT USE - @mli6 - temporary workaround pending release of iOS dark mode
+ }
+
+ private fun noopResetColor(view: ValdiDatePicker, animator: ValdiAnimator?) {
+ // noop
+ // DO NOT USE - @mli6 - temporary workaround pending release of iOS dark mode
+ }
+
+ override fun bindAttributes(attributesBindingContext: AttributesBindingContext) {
+ attributesBindingContext.bindFloatAttribute("dateSeconds", false, this::applyDateSeconds, this::resetDateSeconds)
+ attributesBindingContext.bindFloatAttribute("minimumDateSeconds", false, this::applyMinimumDateSeconds, this::resetMinimumDateSeconds)
+ attributesBindingContext.bindFloatAttribute("maximumDateSeconds", false, this::applyMaximumDateSeconds, this::resetMaximumDateSeconds)
+ attributesBindingContext.bindFunctionAttribute("onChange", this::applyOnChange, this::resetOnChange)
+ attributesBindingContext.bindIntAttribute("preferredStyle", true, this::applyPreferredStyle, this::resetPreferredStyle)
+ attributesBindingContext.bindColorAttribute("color", false, this::noopApplyColor, this::noopResetColor)
+
+ attributesBindingContext.setPlaceholderViewMeasureDelegate(lazy {
+ ValdiDatePicker(context).apply {
+ layoutParams = ViewGroup.LayoutParams(
+ ViewGroup.LayoutParams.WRAP_CONTENT,
+ ViewGroup.LayoutParams.WRAP_CONTENT
+ )
+ }
+ })
+ }
+}
diff --git a/valdi_modules/widgets/android/ValdiEmojiTextView.kt b/valdi_modules/widgets/android/ValdiEmojiTextView.kt
new file mode 100644
index 0000000..24eb8d8
--- /dev/null
+++ b/valdi_modules/widgets/android/ValdiEmojiTextView.kt
@@ -0,0 +1,36 @@
+package com.snap.valdi.views
+
+import android.content.Context
+import android.text.TextDirectionHeuristic
+import androidx.appcompat.widget.AppCompatTextView
+import com.snap.valdi.attributes.impl.richtext.TextViewHelper
+import com.snap.valdi.utils.trace
+
+class ValdiEmojiTextView(context: Context) : AppCompatTextView(context), ValdiRecyclableView, ValdiTextHolder {
+
+ override var textViewHelper: TextViewHelper? = null
+
+ init {
+ TextViewUtils.configure(this)
+ }
+
+ override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
+ trace({"ValdiEmojiTextView.onMeasure"}) {
+ textViewHelper?.onMeasure(widthMeasureSpec, heightMeasureSpec)
+ super.onMeasure(widthMeasureSpec, TextViewUtils.resolveHeightMeasureSpec(this, heightMeasureSpec))
+ }
+ }
+
+ override fun getTextDirectionHeuristic(): TextDirectionHeuristic {
+ return TextViewUtils.resolveTextDirectionHeuristic(super.getTextDirectionHeuristic())
+ }
+
+ override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
+ textViewHelper?.onLayout(changed)
+ super.onLayout(changed, left, top, right, bottom)
+ }
+
+ override fun setTextAccessibility(text: CharSequence?) {
+ super.setText(text, null)
+ }
+}
diff --git a/valdi_modules/widgets/android/ValdiEmojiTextViewAttributesBinder.kt b/valdi_modules/widgets/android/ValdiEmojiTextViewAttributesBinder.kt
new file mode 100644
index 0000000..d54edfc
--- /dev/null
+++ b/valdi_modules/widgets/android/ValdiEmojiTextViewAttributesBinder.kt
@@ -0,0 +1,26 @@
+package com.snap.widgets.text
+
+import android.content.Context
+import android.view.ViewGroup
+import com.snap.valdi.attributes.AttributesBinder
+import com.snap.valdi.attributes.AttributesBindingContext
+import com.snap.valdi.attributes.RegisterAttributesBinder
+import com.snap.valdi.views.ValdiEmojiTextView
+
+@RegisterAttributesBinder
+class ValdiEmojiTextViewAttributesBinder(private val context: Context) : AttributesBinder {
+
+ override val viewClass: Class
+ get() = ValdiEmojiTextView::class.java
+
+ override fun bindAttributes(attributesBindingContext: AttributesBindingContext) {
+ attributesBindingContext.setPlaceholderViewMeasureDelegate(lazy {
+ ValdiEmojiTextView(context).apply {
+ layoutParams = ViewGroup.LayoutParams(
+ ViewGroup.LayoutParams.WRAP_CONTENT,
+ ViewGroup.LayoutParams.WRAP_CONTENT
+ )
+ }
+ })
+ }
+}
diff --git a/valdi_modules/widgets/android/ValdiIndexPickerAttributesBinder.kt b/valdi_modules/widgets/android/ValdiIndexPickerAttributesBinder.kt
new file mode 100644
index 0000000..bfe212c
--- /dev/null
+++ b/valdi_modules/widgets/android/ValdiIndexPickerAttributesBinder.kt
@@ -0,0 +1,62 @@
+package com.snap.widgets.pickers
+
+import android.content.Context
+import android.view.ViewGroup
+import com.snap.valdi.attributes.AttributesBinder
+import com.snap.valdi.attributes.AttributesBindingContext
+import com.snap.valdi.attributes.RegisterAttributesBinder
+import com.snap.valdi.attributes.impl.animations.ValdiAnimator
+import com.snap.valdi.callable.ValdiFunction
+import com.snap.valdi.exceptions.AttributeError
+import com.snap.valdi.views.ValdiIndexPicker
+import com.snapchat.client.valdi_core.AttributeType
+import com.snapchat.client.valdi_core.CompositeAttributePart
+
+@RegisterAttributesBinder
+class ValdiIndexPickerAttributesBinder(private val context: Context) : AttributesBinder {
+
+ override val viewClass: Class
+ get() = ValdiIndexPicker::class.java
+
+ fun applyContent(view: ValdiIndexPicker, value: Any?, animator: ValdiAnimator?) {
+ if (value !is Array<*>) {
+ throw AttributeError("content should be an array")
+ }
+ if (value.size != 2) {
+ throw AttributeError("content should have 2 values in the given array")
+ }
+ val index = value[0] as? Double
+ val labels = value[1] as? Array
+
+ view.setContent(index?.toInt(), labels?.map({ it.toString() })?.toTypedArray())
+ }
+
+ fun resetContent(view: ValdiIndexPicker, animator: ValdiAnimator?) {
+ view.setContent(null, null)
+ }
+
+ private fun applyOnChange(view: ValdiIndexPicker, value: ValdiFunction) {
+ view.onChange = value
+ }
+
+ private fun resetOnChange(view: ValdiIndexPicker) {
+ view.onChange = null
+ }
+
+ override fun bindAttributes(attributesBindingContext: AttributesBindingContext) {
+ attributesBindingContext.bindCompositeAttribute("content", arrayListOf(
+ CompositeAttributePart("index", AttributeType.DOUBLE, true, false),
+ CompositeAttributePart("labels", AttributeType.UNTYPED, false, true)
+ ), this::applyContent, this::resetContent)
+ attributesBindingContext.bindFunctionAttribute("onChange", this::applyOnChange, this::resetOnChange)
+
+ attributesBindingContext.setPlaceholderViewMeasureDelegate(lazy {
+ ValdiIndexPicker(context).apply {
+ layoutParams = ViewGroup.LayoutParams(
+ ViewGroup.LayoutParams.WRAP_CONTENT,
+ ViewGroup.LayoutParams.WRAP_CONTENT
+ )
+ }
+ })
+ }
+}
diff --git a/valdi_modules/widgets/android/ValdiTimePickerAttributesBinder.kt b/valdi_modules/widgets/android/ValdiTimePickerAttributesBinder.kt
new file mode 100644
index 0000000..5783a7c
--- /dev/null
+++ b/valdi_modules/widgets/android/ValdiTimePickerAttributesBinder.kt
@@ -0,0 +1,85 @@
+package com.snap.widgets.pickers
+
+import android.content.Context
+import android.view.ViewGroup
+import com.snap.valdi.attributes.AttributesBinder
+import com.snap.valdi.attributes.AttributesBindingContext
+import com.snap.valdi.attributes.RegisterAttributesBinder
+import com.snap.valdi.attributes.impl.animations.ValdiAnimator
+import com.snap.valdi.callable.ValdiFunction
+import com.snap.valdi.views.ValdiTimePicker
+
+@RegisterAttributesBinder
+class ValdiTimePickerAttributesBinder(private val context: Context) : AttributesBinder {
+
+ override val viewClass: Class
+ get() = ValdiTimePicker::class.java
+
+ fun applyHourOfDay(view: ValdiTimePicker, value: Int, animator: ValdiAnimator?) {
+ view.hourOfDay = value
+ }
+
+ fun resetHourOfDay(view: ValdiTimePicker, animator: ValdiAnimator?) {
+ view.hourOfDay = null
+ }
+
+ fun applyMinuteOfHour(view: ValdiTimePicker, value: Int, animator: ValdiAnimator?) {
+ view.minuteOfHour = value
+ }
+
+ fun resetMinuteOfHour(view: ValdiTimePicker, animator: ValdiAnimator?) {
+ view.minuteOfHour = null
+ }
+
+ fun applyInterval(view: ValdiTimePicker, value: Int, animator: ValdiAnimator?) {
+ view.intervalMinutes = value
+ }
+
+ fun resetInterval(view: ValdiTimePicker, animator: ValdiAnimator?) {
+ view.intervalMinutes = 1
+ }
+
+ private fun applyOnChange(view: ValdiTimePicker, fn: ValdiFunction) {
+ view.onChangeFunction = fn
+ }
+
+ private fun resetOnChange(view: ValdiTimePicker) {
+ view.onChangeFunction = null
+ }
+
+ private fun applyPreferredStyle(view: ValdiTimePicker, value: Int, animator: ValdiAnimator?) {
+ view.preferredStyle = value
+ }
+
+ private fun resetPreferredStyle(view: ValdiTimePicker, animator: ValdiAnimator?) {
+ view.preferredStyle = 1 // spinner
+ }
+
+ private fun noopApplyColor(view: ValdiTimePicker, value: Int, animator: ValdiAnimator?) {
+ // noop
+ // DO NOT USE - @mli6 - temporary workaround pending release of iOS dark mode
+ }
+
+ private fun noopResetColor(view: ValdiTimePicker, animator: ValdiAnimator?) {
+ // noop
+ // DO NOT USE - @mli6 - temporary workaround pending release of iOS dark mode
+ }
+
+ override fun bindAttributes(attributesBindingContext: AttributesBindingContext) {
+ attributesBindingContext.bindIntAttribute("hourOfDay", false, this::applyHourOfDay, this::resetHourOfDay)
+ attributesBindingContext.bindIntAttribute("minuteOfHour", false, this::applyMinuteOfHour, this::resetMinuteOfHour)
+ attributesBindingContext.bindIntAttribute("intervalMinutes", false, this::applyInterval, this::resetInterval)
+ attributesBindingContext.bindFunctionAttribute("onChange", this::applyOnChange, this::resetOnChange)
+ attributesBindingContext.bindIntAttribute("preferredStyle", true, this::applyPreferredStyle, this::resetPreferredStyle)
+ attributesBindingContext.bindColorAttribute("color", false, this::noopApplyColor, this::noopResetColor)
+
+ attributesBindingContext.setPlaceholderViewMeasureDelegate(lazy {
+ ValdiTimePicker(context).apply {
+ layoutParams = ViewGroup.LayoutParams(
+ ViewGroup.LayoutParams.WRAP_CONTENT,
+ ViewGroup.LayoutParams.WRAP_CONTENT
+ )
+ }
+ })
+ }
+}
diff --git a/valdi_modules/widgets/ios/SCWidgetsDatePicker.h b/valdi_modules/widgets/ios/SCWidgetsDatePicker.h
new file mode 100644
index 0000000..ccceceb
--- /dev/null
+++ b/valdi_modules/widgets/ios/SCWidgetsDatePicker.h
@@ -0,0 +1,16 @@
+//
+// SCWidgetsDatePicker.h
+// valdi-ios
+//
+// Created by Saniul Ahmed on 15/07/2020.
+//
+
+#import
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface SCWidgetsDatePicker : UIDatePicker
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/valdi_modules/widgets/ios/SCWidgetsDatePicker.m b/valdi_modules/widgets/ios/SCWidgetsDatePicker.m
new file mode 100644
index 0000000..2938f28
--- /dev/null
+++ b/valdi_modules/widgets/ios/SCWidgetsDatePicker.m
@@ -0,0 +1,128 @@
+//
+// SCWidgetsDatePicker.m
+// Valdi_Widgets
+//
+
+#import "SCWidgetsDatePicker.h"
+#import "SCWidgetsDatePickerUtils.h"
+
+#import "valdi_core/SCValdiAttributesBinderBase.h"
+#import "valdi_core/SCValdiFunction.h"
+#import "valdi_core/SCValdiMarshaller.h"
+
+@implementation SCWidgetsDatePicker {
+ id _Nullable _onChange;
+}
+
+- (instancetype)initWithFrame:(CGRect)frame {
+ self = [super initWithFrame:frame];
+ if (self) {
+ self.datePickerMode = UIDatePickerModeDate;
+ SCWidgetsFixIOS14DatePicker(self);
+ [self addTarget:self action:@selector(_handleOnChange) forControlEvents:UIControlEventValueChanged];
+ }
+ return self;
+}
+
+- (CGSize)sizeThatFits:(CGSize)size
+{
+ if (@available(iOS 13.4, *)) {
+ if (self.preferredDatePickerStyle == UIDatePickerStyleCompact) {
+ CGSize fittingSize = [self systemLayoutSizeFittingSize:UILayoutFittingCompressedSize
+ withHorizontalFittingPriority:UILayoutPriorityFittingSizeLevel
+ verticalFittingPriority:UILayoutPriorityFittingSizeLevel];
+ if (fittingSize.width > 0 && fittingSize.height > 0) {
+ return fittingSize;
+ }
+ }
+ }
+ return [super sizeThatFits:size];
+}
+
+#pragma mark - Internal methods
+
+- (void)_handleOnChange
+{
+ if (!_onChange) {
+ return;
+ }
+
+ NSTimeInterval dateSeconds = self.date.timeIntervalSince1970;
+ SCValdiMarshallerScoped(marshaller, {
+ NSInteger objectIndex = SCValdiMarshallerPushMap(marshaller, 1);
+ SCValdiMarshallerPushDouble(marshaller, dateSeconds);
+ SCValdiMarshallerPutMapPropertyUninterned(marshaller, @"dateSeconds", objectIndex);
+
+ [_onChange performWithMarshaller:marshaller];
+ });
+}
+
+#pragma mark - Static methods
+
++ (void)bindAttributes:(id)attributesBinder {
+ [attributesBinder bindAttribute:@"dateSeconds"
+ invalidateLayoutOnChange:NO
+ withDoubleBlock:^BOOL(SCWidgetsDatePicker *view, CGFloat attributeValue, id animator) {
+ NSDate *date = [NSDate dateWithTimeIntervalSince1970:attributeValue];
+ BOOL animated = animator != nil;
+ [view setDate:date animated:animated];
+ return YES;
+ } resetBlock:^(SCWidgetsDatePicker *view, id animator) {
+ BOOL animated = animator != nil;
+ [view setDate:[NSDate date] animated:animated];
+ }];
+
+ [attributesBinder bindAttribute:@"minimumDateSeconds"
+ invalidateLayoutOnChange:NO
+ withDoubleBlock:^BOOL(SCWidgetsDatePicker *view, CGFloat attributeValue, id animator) {
+ NSDate *minDate = [NSDate dateWithTimeIntervalSince1970:attributeValue];
+ view.minimumDate = minDate;
+ return YES;
+ } resetBlock:^(SCWidgetsDatePicker *view, id animator) {
+ view.minimumDate = nil;
+ }];
+
+ [attributesBinder bindAttribute:@"maximumDateSeconds"
+ invalidateLayoutOnChange:NO
+ withDoubleBlock:^BOOL(SCWidgetsDatePicker *view, CGFloat attributeValue, id animator) {
+ NSDate *maxDate = [NSDate dateWithTimeIntervalSince1970:attributeValue];
+ view.maximumDate = maxDate;
+ return YES;
+ } resetBlock:^(SCWidgetsDatePicker *view, id animator) {
+ view.maximumDate = nil;
+ }];
+
+ [attributesBinder bindAttribute:@"onChange"
+ withFunctionBlock:^(SCWidgetsDatePicker *view, id attributeValue) {
+ view->_onChange = attributeValue;
+ }
+ resetBlock:^(SCWidgetsDatePicker *view) {
+ view->_onChange = nil;
+ }];
+
+ [attributesBinder bindAttribute:@"color"
+ invalidateLayoutOnChange:NO
+ withColorBlock:^BOOL(SCWidgetsDatePicker *view, UIColor *attributeValue, id animator) {
+ [view setValue:attributeValue forKey:@"textColor"];
+ [view setValue:@(NO) forKey:@"highlightsToday"];
+ return YES;
+ }
+ resetBlock:^(SCWidgetsDatePicker *view, id animator) {
+ [view setValue:nil forKey:@"textColor"];
+ }];
+
+ [attributesBinder bindAttribute:@"preferredStyle"
+ invalidateLayoutOnChange:YES
+ withIntBlock:^BOOL(SCWidgetsDatePicker *view, NSInteger attributeValue, id animator) {
+ SCWidgetsSetDatePickerStyle(view, attributeValue);
+ return YES;
+ } resetBlock:^(SCWidgetsDatePicker *view, id animator) {
+ SCWidgetsSetDatePickerStyle(view, 1 /* wheels */);
+ }];
+
+ [attributesBinder setPlaceholderViewMeasureDelegate:^UIView *{
+ return [SCWidgetsDatePicker new];
+ }];
+}
+
+@end
diff --git a/valdi_modules/widgets/ios/SCWidgetsDatePickerUtils.h b/valdi_modules/widgets/ios/SCWidgetsDatePickerUtils.h
new file mode 100644
index 0000000..6ecca90
--- /dev/null
+++ b/valdi_modules/widgets/ios/SCWidgetsDatePickerUtils.h
@@ -0,0 +1,14 @@
+#import
+
+NS_ASSUME_NONNULL_BEGIN
+
+/// UIDatePicker defaults to a new "compact" date picker style starting from iOS 14,
+/// this function configures a UIDatePicker instance to use the old style.
+void SCWidgetsFixIOS14DatePicker(UIDatePicker* datePicker);
+
+/// Sets the preferred date picker style on iOS 14+.
+/// Values: 1 = spinner (wheels), 2 = overlay (compact), 3 = expanded (inline).
+/// Source of truth: STYLE_MAP in composer/coreui/src/components/pickers/DatePickerPreferredStyle.ts
+void SCWidgetsSetDatePickerStyle(UIDatePicker* datePicker, NSInteger style);
+
+NS_ASSUME_NONNULL_END
diff --git a/valdi_modules/widgets/ios/SCWidgetsDatePickerUtils.m b/valdi_modules/widgets/ios/SCWidgetsDatePickerUtils.m
new file mode 100644
index 0000000..948bbb3
--- /dev/null
+++ b/valdi_modules/widgets/ios/SCWidgetsDatePickerUtils.m
@@ -0,0 +1,34 @@
+#import
+
+#ifndef __IPHONE_14_0
+#define __IPHONE_14_0 140000
+#endif
+
+void SCWidgetsSetDatePickerStyle(UIDatePicker *datePicker, NSInteger style) {
+ // The `client` artifact currently still compiles using Xcode 11.4.1, but
+ // it might already be linked into an app built with Xcode 12, which will trigger
+ // the new behavior. So, we have to set the preferredDatePickerStyle value even
+ // if we don't have access to it in the UIKit headers.
+ //
+ // That's why we're doing a build-time API availability check in addition to the
+ // runtime availability check.
+ #if __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_14_0
+ if (@available(iOS 13.4, *)) {
+ datePicker.preferredDatePickerStyle = (UIDatePickerStyle)style;
+ }
+ #else
+ SEL setPreferredDatePickerStyleSelector = NSSelectorFromString(@"setPreferredDatePickerStyle:");
+ if (setPreferredDatePickerStyleSelector && [datePicker respondsToSelector:setPreferredDatePickerStyleSelector]) {
+ NSMethodSignature* signature = [[datePicker class] instanceMethodSignatureForSelector:setPreferredDatePickerStyleSelector];
+ NSInvocation* invocation = [NSInvocation invocationWithMethodSignature:signature];
+ [invocation setTarget:datePicker];
+ [invocation setSelector:setPreferredDatePickerStyleSelector];
+ [invocation setArgument:&style atIndex:2];
+ [invocation invoke];
+ }
+ #endif
+}
+
+void SCWidgetsFixIOS14DatePicker(UIDatePicker *datePicker) {
+ SCWidgetsSetDatePickerStyle(datePicker, 1 /* wheels */);
+}
diff --git a/valdi_modules/widgets/ios/SCWidgetsDateTimePicker.h b/valdi_modules/widgets/ios/SCWidgetsDateTimePicker.h
new file mode 100644
index 0000000..25ec516
--- /dev/null
+++ b/valdi_modules/widgets/ios/SCWidgetsDateTimePicker.h
@@ -0,0 +1,9 @@
+#import
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface SCWidgetsDateTimePicker : UIDatePicker
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/valdi_modules/widgets/ios/SCWidgetsDateTimePicker.m b/valdi_modules/widgets/ios/SCWidgetsDateTimePicker.m
new file mode 100644
index 0000000..ba3d7d9
--- /dev/null
+++ b/valdi_modules/widgets/ios/SCWidgetsDateTimePicker.m
@@ -0,0 +1,88 @@
+#import "SCWidgetsDateTimePicker.h"
+#import "SCWidgetsDatePickerUtils.h"
+
+#import "valdi_core/SCValdiAttributesBinderBase.h"
+#import "valdi_core/SCValdiFunction.h"
+#import "valdi_core/SCValdiMarshaller.h"
+
+@implementation SCWidgetsDateTimePicker {
+ id _Nullable _onChange;
+}
+
+- (instancetype)initWithFrame:(CGRect)frame {
+ self = [super initWithFrame:frame];
+ if (self) {
+ self.datePickerMode = UIDatePickerModeDateAndTime;
+ SCWidgetsFixIOS14DatePicker(self);
+ [self addTarget:self action:@selector(_handleOnChange) forControlEvents:UIControlEventValueChanged];
+ }
+ return self;
+}
+
+#pragma mark - Internal methods
+
+- (void)_handleOnChange
+{
+ if (!_onChange) {
+ return;
+ }
+
+ NSTimeInterval dateSeconds = self.date.timeIntervalSince1970;
+ SCValdiMarshallerScoped(marshaller, {
+ NSInteger objectIndex = SCValdiMarshallerPushMap(marshaller, 1);
+ SCValdiMarshallerPushDouble(marshaller, dateSeconds);
+ SCValdiMarshallerPutMapPropertyUninterned(marshaller, @"dateSeconds", objectIndex);
+
+ [_onChange performWithMarshaller:marshaller];
+ });
+}
+
+#pragma mark - Static methods
+
++ (void)bindAttributes:(id)attributesBinder {
+ [attributesBinder bindAttribute:@"dateSeconds"
+ invalidateLayoutOnChange:NO
+ withDoubleBlock:^BOOL(SCWidgetsDateTimePicker *view, CGFloat attributeValue, id animator) {
+ NSDate *date = [NSDate dateWithTimeIntervalSince1970:attributeValue];
+ BOOL animated = animator != nil;
+ [view setDate:date animated:animated];
+ return YES;
+ } resetBlock:^(SCWidgetsDateTimePicker *view, id animator) {
+ BOOL animated = animator != nil;
+ [view setDate:[NSDate date] animated:animated];
+ }];
+
+ [attributesBinder bindAttribute:@"minimumDateSeconds"
+ invalidateLayoutOnChange:NO
+ withDoubleBlock:^BOOL(SCWidgetsDateTimePicker *view, CGFloat attributeValue, id animator) {
+ NSDate *minDate = [NSDate dateWithTimeIntervalSince1970:attributeValue];
+ view.minimumDate = minDate;
+ return YES;
+ } resetBlock:^(SCWidgetsDateTimePicker *view, id animator) {
+ view.minimumDate = nil;
+ }];
+
+ [attributesBinder bindAttribute:@"maximumDateSeconds"
+ invalidateLayoutOnChange:NO
+ withDoubleBlock:^BOOL(SCWidgetsDateTimePicker *view, CGFloat attributeValue, id animator) {
+ NSDate *maxDate = [NSDate dateWithTimeIntervalSince1970:attributeValue];
+ view.maximumDate = maxDate;
+ return YES;
+ } resetBlock:^(SCWidgetsDateTimePicker *view, id animator) {
+ view.maximumDate = nil;
+ }];
+
+ [attributesBinder bindAttribute:@"onChange"
+ withFunctionBlock:^(SCWidgetsDateTimePicker *view, id attributeValue) {
+ view->_onChange = attributeValue;
+ }
+ resetBlock:^(SCWidgetsDateTimePicker *view) {
+ view->_onChange = nil;
+ }];
+
+ [attributesBinder setPlaceholderViewMeasureDelegate:^UIView *{
+ return [SCWidgetsDateTimePicker new];
+ }];
+}
+
+@end
diff --git a/valdi_modules/widgets/ios/SCWidgetsIndexPicker.h b/valdi_modules/widgets/ios/SCWidgetsIndexPicker.h
new file mode 100644
index 0000000..865ddb3
--- /dev/null
+++ b/valdi_modules/widgets/ios/SCWidgetsIndexPicker.h
@@ -0,0 +1,16 @@
+//
+// SCWidgetsIndexPicker.h
+// valdi-ios
+//
+// Created by Vincent Brunet on 03/16/21.
+//
+
+#import
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface SCWidgetsIndexPicker : UIPickerView
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/valdi_modules/widgets/ios/SCWidgetsIndexPicker.m b/valdi_modules/widgets/ios/SCWidgetsIndexPicker.m
new file mode 100644
index 0000000..a3eab9c
--- /dev/null
+++ b/valdi_modules/widgets/ios/SCWidgetsIndexPicker.m
@@ -0,0 +1,137 @@
+//
+// SCWidgetsIndexPicker.m
+// Valdi_Widgets
+//
+
+#import "SCWidgetsIndexPicker.h"
+
+#import "valdi_core/SCValdiAttributesBinderBase.h"
+#import "valdi_core/SCValdiFunction.h"
+#import "valdi_core/SCValdiMarshaller.h"
+#import "valdi_core/SCNValdiCoreCompositeAttributePart.h"
+
+@implementation SCWidgetsIndexPicker {
+ NSArray* _labels;
+ id _Nullable _onChange;
+}
+
+- (instancetype)initWithFrame:(CGRect)frame
+{
+ self = [super initWithFrame:frame];
+ if (self) {
+ self.delegate = self;
+ self.dataSource = self;
+ }
+ return self;
+}
+
+#pragma mark - Data source
+
+- (NSInteger)numberOfComponentsInPickerView:(UIPickerView *)pickerView
+{
+ return 1;
+}
+
+- (NSInteger)pickerView:(UIPickerView *)pickerView numberOfRowsInComponent:(NSInteger)component
+{
+ return [_labels count];
+}
+
+- (NSString *)pickerView:(UIPickerView *)pickerView titleForRow:(NSInteger)row forComponent:(NSInteger)component
+{
+ if (row < 0) {
+ return @"--";
+ }
+ if ([_labels count] <= (NSUInteger)row) {
+ return @"--";
+ }
+ return [_labels objectAtIndex:row];
+}
+
+#pragma mark - Data events
+
+- (void)pickerView:(UIPickerView *)pickerView didSelectRow:(NSInteger)row inComponent:(NSInteger)component
+{
+ if (!_onChange) {
+ return;
+ }
+ SCValdiMarshallerScoped(marshaller, {
+ SCValdiMarshallerPushDouble(marshaller, row);
+ [_onChange performWithMarshaller:marshaller];
+ });
+}
+
+#pragma mark - Internal methods
+
+- (BOOL)_setContent:(NSNumber *)index labels:(NSArray *)labels animator:(id)animator
+{
+ if (![_labels isEqual:labels]) {
+ for (id label in labels) {
+ if (![label isKindOfClass:[NSString class]]) {
+ return NO;
+ }
+ }
+ _labels = labels;
+ [self reloadAllComponents];
+ }
+
+ NSInteger size = [labels count];
+
+ NSInteger newIndex = MAX(0, MIN(size - 1, [index integerValue]));
+ if (newIndex != [self selectedRowInComponent:0]) {
+ [self selectRow:newIndex inComponent:0 animated:animator != nil];
+ }
+
+ return YES;
+}
+
+#pragma mark - Static methods
+
++ (NSArray *)_contentComponents
+{
+ return @[
+ [[SCNValdiCoreCompositeAttributePart alloc] initWithAttribute:@"index"
+ type:SCNValdiCoreAttributeTypeDouble
+ optional:YES
+ invalidateLayoutOnChange:NO],
+ [[SCNValdiCoreCompositeAttributePart alloc] initWithAttribute:@"labels"
+ type:SCNValdiCoreAttributeTypeUntyped
+ optional:NO
+ invalidateLayoutOnChange:NO],
+ ];
+}
+
++ (void)bindAttributes:(id)attributesBinder {
+
+ [attributesBinder bindCompositeAttribute:@"content"
+ parts:[SCWidgetsIndexPicker _contentComponents]
+ withUntypedBlock:^BOOL(SCWidgetsIndexPicker *view, id attributeValue, id animator) {
+ NSArray *attributeValueArray = [attributeValue isKindOfClass:[NSArray class]] ? attributeValue : nil;
+ if (attributeValueArray.count != 2) {
+ return NO;
+ }
+
+ NSNumber *index = [attributeValueArray[0] isKindOfClass:[NSNumber class]] ? attributeValueArray[0] : nil;
+ NSArray *labels = [attributeValueArray[1] isKindOfClass:[NSArray class]] ? attributeValueArray[1] : nil;
+
+ return [view _setContent:index labels:labels animator:animator];
+ }
+ resetBlock:^(SCWidgetsIndexPicker *view, id animator) {
+ [view _setContent:nil labels:nil animator:animator];
+ }];
+
+ [attributesBinder bindAttribute:@"onChange"
+ withFunctionBlock:^(SCWidgetsIndexPicker *view, id attributeValue) {
+ view->_onChange = attributeValue;
+ }
+ resetBlock:^(SCWidgetsIndexPicker *view) {
+ view->_onChange = nil;
+ }];
+
+ [attributesBinder setPlaceholderViewMeasureDelegate:^UIView *{
+ return [SCWidgetsIndexPicker new];
+ }];
+
+}
+
+@end
diff --git a/valdi_modules/widgets/ios/SCWidgetsLabel.h b/valdi_modules/widgets/ios/SCWidgetsLabel.h
new file mode 100644
index 0000000..c8e6a47
--- /dev/null
+++ b/valdi_modules/widgets/ios/SCWidgetsLabel.h
@@ -0,0 +1,14 @@
+//
+// SCWidgetsLabel.h
+// Valdi_Widgets
+//
+
+#import
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface SCWidgetsLabel : UILabel
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/valdi_modules/widgets/ios/SCWidgetsLabel.m b/valdi_modules/widgets/ios/SCWidgetsLabel.m
new file mode 100644
index 0000000..d8c03fe
--- /dev/null
+++ b/valdi_modules/widgets/ios/SCWidgetsLabel.m
@@ -0,0 +1,113 @@
+//
+// SCWidgetsLabel.m
+// Valdi_Widgets
+//
+
+#import "SCWidgetsLabel.h"
+
+#import "valdi_core/SCValdiAttributesBinderBase.h"
+
+static UIFont *SCWidgetsResolveFont(NSString *str) {
+ if (![str isKindOfClass:[NSString class]]) return nil;
+ NSArray *parts = [str componentsSeparatedByString:@" "];
+ if (parts.count < 2) return nil;
+ NSString *name = parts[0];
+ CGFloat size = [parts[1] doubleValue];
+ if ([name isEqualToString:@"system-bold"]) {
+ return [UIFont boldSystemFontOfSize:size];
+ }
+ if ([name isEqualToString:@"system"]) {
+ return [UIFont systemFontOfSize:size];
+ }
+ return [UIFont fontWithName:name size:size];
+}
+
+@implementation SCWidgetsLabel
+
+- (instancetype)initWithFrame:(CGRect)frame
+{
+ self = [super initWithFrame:frame];
+ if (self) {
+ self.shadowOffset = CGSizeMake(0, 0);
+ self.userInteractionEnabled = YES;
+ self.adjustsFontForContentSizeCategory = NO;
+ }
+ return self;
+}
+
+- (UIAccessibilityTraits)accessibilityTraits
+{
+ UIAccessibilityTraits traits = [super accessibilityTraits];
+ traits &= ~UIAccessibilityTraitButton;
+ traits |= UIAccessibilityTraitStaticText;
+ return traits;
+}
+
+#pragma mark - Static methods
+
++ (void)bindAttributes:(id)attributesBinder
+{
+ [attributesBinder bindAttribute:@"value"
+ invalidateLayoutOnChange:YES
+ withStringBlock:^BOOL(SCWidgetsLabel *view, NSString *attributeValue, id animator) {
+ view.text = attributeValue;
+ return YES;
+ } resetBlock:^(SCWidgetsLabel *view, id animator) {
+ view.text = nil;
+ }];
+
+ [attributesBinder bindAttribute:@"color"
+ invalidateLayoutOnChange:NO
+ withColorBlock:^BOOL(SCWidgetsLabel *view, UIColor *attributeValue, id animator) {
+ view.textColor = attributeValue;
+ return YES;
+ }
+ resetBlock:^(SCWidgetsLabel *view, id animator) {
+ view.textColor = [UIColor labelColor];
+ }];
+
+ [attributesBinder bindAttribute:@"font"
+ invalidateLayoutOnChange:YES
+ withUntypedBlock:^BOOL(SCWidgetsLabel *view, id attributeValue, id animator) {
+ UIFont *font = SCWidgetsResolveFont(attributeValue);
+ view.font = font ?: [UIFont systemFontOfSize:[UIFont systemFontSize]];
+ return YES;
+ } resetBlock:^(SCWidgetsLabel *view, id animator) {
+ view.font = [UIFont systemFontOfSize:[UIFont systemFontSize]];
+ }];
+
+ [attributesBinder bindAttribute:@"numberOfLines"
+ invalidateLayoutOnChange:YES
+ withIntBlock:^BOOL(SCWidgetsLabel *view, NSInteger attributeValue, id animator) {
+ view.numberOfLines = attributeValue;
+ return YES;
+ } resetBlock:^(SCWidgetsLabel *view, id animator) {
+ view.numberOfLines = 1;
+ }];
+
+ [attributesBinder bindAttribute:@"adjustsFontSizeToFitWidth"
+ invalidateLayoutOnChange:YES
+ withBoolBlock:^BOOL(UILabel *label, BOOL attributeValue, id animator) {
+ label.adjustsFontSizeToFitWidth = attributeValue;
+ return YES;
+ }
+ resetBlock:^(UILabel *label, id animator) {
+ label.adjustsFontSizeToFitWidth = NO;
+ }];
+
+ [attributesBinder bindAttribute:@"minimumScaleFactor"
+ invalidateLayoutOnChange:YES
+ withDoubleBlock:^BOOL(UILabel *label, CGFloat attributeValue, id animator) {
+ label.minimumScaleFactor = attributeValue;
+ return YES;
+ }
+ resetBlock:^(UILabel *label, id animator) {
+ label.minimumScaleFactor = 0;
+ }];
+
+ [attributesBinder setPlaceholderViewMeasureDelegate:^UIView *{
+ return [SCWidgetsLabel new];
+ }];
+}
+
+@end
diff --git a/valdi_modules/widgets/ios/SCWidgetsTimePicker.h b/valdi_modules/widgets/ios/SCWidgetsTimePicker.h
new file mode 100644
index 0000000..0bbbefe
--- /dev/null
+++ b/valdi_modules/widgets/ios/SCWidgetsTimePicker.h
@@ -0,0 +1,16 @@
+//
+// SCWidgetsTimePicker.h
+// valdi-ios
+//
+// Created by Saniul Ahmed on 15/07/2020.
+//
+
+#import
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface SCWidgetsTimePicker : UIDatePicker
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/valdi_modules/widgets/ios/SCWidgetsTimePicker.m b/valdi_modules/widgets/ios/SCWidgetsTimePicker.m
new file mode 100644
index 0000000..cb6391a
--- /dev/null
+++ b/valdi_modules/widgets/ios/SCWidgetsTimePicker.m
@@ -0,0 +1,157 @@
+//
+// SCWidgetsTimePicker.m
+// Valdi_Widgets
+//
+
+#import "SCWidgetsTimePicker.h"
+#import "SCWidgetsDatePickerUtils.h"
+
+#import "valdi_core/SCValdiAttributesBinderBase.h"
+#import "valdi_core/SCValdiFunction.h"
+#import "valdi_core/SCValdiMarshaller.h"
+
+@implementation SCWidgetsTimePicker {
+ id _Nullable _onChange;
+}
+
+- (instancetype)initWithFrame:(CGRect)frame {
+ self = [super initWithFrame:frame];
+ if (self) {
+ self.datePickerMode = UIDatePickerModeTime;
+ NSCalendar *calendar = [[NSCalendar currentCalendar] copy];
+ calendar.timeZone = [NSTimeZone timeZoneWithName:@"UTC"];
+ self.calendar = calendar;
+ self.timeZone = self.calendar.timeZone;
+ SCWidgetsFixIOS14DatePicker(self);
+ [self addTarget:self action:@selector(_handleOnChange) forControlEvents:UIControlEventValueChanged];
+ }
+ return self;
+}
+
+- (CGSize)sizeThatFits:(CGSize)size
+{
+ if (@available(iOS 13.4, *)) {
+ if (self.preferredDatePickerStyle == UIDatePickerStyleCompact) {
+ CGSize fittingSize = [self systemLayoutSizeFittingSize:UILayoutFittingCompressedSize
+ withHorizontalFittingPriority:UILayoutPriorityFittingSizeLevel
+ verticalFittingPriority:UILayoutPriorityFittingSizeLevel];
+ if (fittingSize.width > 0 && fittingSize.height > 0) {
+ return fittingSize;
+ }
+ }
+ }
+ return [super sizeThatFits:size];
+}
+
+#pragma mark - Internal methods
+
+- (void)_handleOnChange
+{
+ if (!_onChange) {
+ return;
+ }
+
+ NSDateComponents *components = [self.calendar components:(NSCalendarUnitHour | NSCalendarUnitMinute)
+ fromDate:self.date];
+ NSInteger hourOfDay = components.hour;
+ NSInteger minuteOfHour = components.minute;
+
+ SCValdiMarshallerScoped(marshaller, {
+ NSInteger objectIndex = SCValdiMarshallerPushMap(marshaller, 2);
+ SCValdiMarshallerPushInt(marshaller, (int32_t)hourOfDay);
+ SCValdiMarshallerPutMapPropertyUninterned(marshaller, @"hourOfDay", objectIndex);
+ SCValdiMarshallerPushInt(marshaller, (int32_t)minuteOfHour);
+ SCValdiMarshallerPutMapPropertyUninterned(marshaller, @"minuteOfHour", objectIndex);
+
+ [_onChange performWithMarshaller:marshaller];
+ });
+}
+
+- (NSDate *)_dateFromDate:(NSDate *)baseDate withCalendarComponent:(NSCalendarUnit)component setTo:(NSInteger)value
+{
+ NSCalendar *calendar = self.calendar;
+ NSDateComponents *hourMinuteComponents = [calendar components:NSCalendarUnitHour | NSCalendarUnitMinute | NSCalendarUnitSecond fromDate:baseDate];
+ [hourMinuteComponents setValue:value forComponent:component];
+ NSDate *newDate = [calendar dateBySettingHour:hourMinuteComponents.hour
+ minute:hourMinuteComponents.minute
+ second:hourMinuteComponents.second
+ ofDate:baseDate
+ options:0];
+
+ if (newDate == nil) {
+ newDate = [NSDate date];
+ }
+
+ return newDate;
+}
+
+#pragma mark - Static methods
+
++ (void)bindAttributes:(id)attributesBinder {
+ [attributesBinder bindAttribute:@"hourOfDay"
+ invalidateLayoutOnChange:NO
+ withIntBlock:^BOOL(SCWidgetsTimePicker *view, NSInteger attributeValue, id animator) {
+ NSDate *date = [view _dateFromDate:view.date withCalendarComponent:NSCalendarUnitHour setTo:attributeValue];
+ BOOL animated = animator != nil;
+ [view setDate:date animated:animated];
+ return YES;
+ } resetBlock:^(SCWidgetsTimePicker *view, id animator) {
+ NSDate *date = [view _dateFromDate:view.date withCalendarComponent:NSCalendarUnitHour setTo:9];
+ BOOL animated = animator != nil;
+ [view setDate:date animated:animated];
+ }];
+
+ [attributesBinder bindAttribute:@"minuteOfHour"
+ invalidateLayoutOnChange:NO
+ withIntBlock:^BOOL(SCWidgetsTimePicker *view, NSInteger attributeValue, id animator) {
+ NSDate *date = [view _dateFromDate:view.date withCalendarComponent:NSCalendarUnitMinute setTo:attributeValue];
+ BOOL animated = animator != nil;
+ [view setDate:date animated:animated];
+ return YES;
+ } resetBlock:^(SCWidgetsTimePicker *view, id animator) {
+ NSDate *date = [view _dateFromDate:view.date withCalendarComponent:NSCalendarUnitMinute setTo:41];
+ BOOL animated = animator != nil;
+ [view setDate:date animated:animated];
+ }];
+
+ [attributesBinder bindAttribute:@"intervalMinutes" invalidateLayoutOnChange:NO withIntBlock:^BOOL(SCWidgetsTimePicker *view, NSInteger attributeValue, id animator) {
+ [view setMinuteInterval:attributeValue];
+ return YES;
+ } resetBlock:^(SCWidgetsTimePicker *view, id animator) {
+ [view setMinuteInterval:1];
+ }];
+
+ [attributesBinder bindAttribute:@"onChange"
+ withFunctionBlock:^(SCWidgetsTimePicker *view, id attributeValue) {
+ view->_onChange = attributeValue;
+ }
+ resetBlock:^(SCWidgetsTimePicker *view) {
+ view->_onChange = nil;
+ }];
+
+ [attributesBinder bindAttribute:@"color"
+ invalidateLayoutOnChange:NO
+ withColorBlock:^BOOL(SCWidgetsTimePicker *view, UIColor *attributeValue, id animator) {
+ [view setValue:attributeValue forKey:@"textColor"];
+ [view setValue:@(NO) forKey:@"highlightsToday"];
+ return YES;
+ }
+ resetBlock:^(SCWidgetsTimePicker *view, id animator) {
+ [view setValue:nil forKey:@"textColor"];
+ }];
+
+ [attributesBinder bindAttribute:@"preferredStyle"
+ invalidateLayoutOnChange:YES
+ withIntBlock:^BOOL(SCWidgetsTimePicker *view, NSInteger attributeValue, id animator) {
+ SCWidgetsSetDatePickerStyle(view, attributeValue);
+ return YES;
+ } resetBlock:^(SCWidgetsTimePicker *view, id animator) {
+ SCWidgetsSetDatePickerStyle(view, 1 /* wheels */);
+ }];
+
+ [attributesBinder setPlaceholderViewMeasureDelegate:^UIView *{
+ return [SCWidgetsTimePicker new];
+ }];
+}
+
+@end
diff --git a/valdi_modules/widgets/macos/SCWidgetsMacOSDatePicker.h b/valdi_modules/widgets/macos/SCWidgetsMacOSDatePicker.h
new file mode 100644
index 0000000..f5faad8
--- /dev/null
+++ b/valdi_modules/widgets/macos/SCWidgetsMacOSDatePicker.h
@@ -0,0 +1,17 @@
+//
+// SCWidgetsMacOSDatePicker.h
+// valdi-widgets-macos
+//
+
+#import "valdi/macos/SCValdiMacOSAttributesBinder.h"
+#import
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface SCWidgetsMacOSDatePicker : NSDatePicker
+
++ (void)bindAttributes:(SCValdiMacOSAttributesBinder *)attributesBinder;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/valdi_modules/widgets/macos/SCWidgetsMacOSDatePicker.m b/valdi_modules/widgets/macos/SCWidgetsMacOSDatePicker.m
new file mode 100644
index 0000000..eefc2fd
--- /dev/null
+++ b/valdi_modules/widgets/macos/SCWidgetsMacOSDatePicker.m
@@ -0,0 +1,71 @@
+//
+// SCWidgetsMacOSDatePicker.m
+// valdi-widgets-macos
+//
+
+#import "SCWidgetsMacOSDatePicker.h"
+#import "valdi/macos/SCValdiMacOSFunction.h"
+
+@implementation SCWidgetsMacOSDatePicker {
+ SCValdiMacOSFunction *_onChange;
+}
+
+- (instancetype)initWithFrame:(NSRect)frameRect {
+ self = [super initWithFrame:frameRect];
+ if (self) {
+ self.datePickerElements = NSDatePickerElementFlagYearMonthDay;
+ self.datePickerStyle = NSDatePickerStyleTextField;
+ self.target = self;
+ self.action = @selector(_handleOnChange);
+ }
+ return self;
+}
+
+- (void)_handleOnChange {
+ if (!_onChange) return;
+ NSTimeInterval dateSeconds = self.dateValue.timeIntervalSince1970;
+ [_onChange performWithParameters:@[@{@"dateSeconds": @(dateSeconds)}]];
+}
+
+- (void)valdi_setDateSeconds:(id)value {
+ if ([value isKindOfClass:[NSNumber class]]) {
+ self.dateValue = [NSDate dateWithTimeIntervalSince1970:[(NSNumber *)value doubleValue]];
+ }
+}
+
+- (void)valdi_setMinimumDateSeconds:(id)value {
+ if ([value isKindOfClass:[NSNumber class]]) {
+ self.minDate = [NSDate dateWithTimeIntervalSince1970:[(NSNumber *)value doubleValue]];
+ } else {
+ self.minDate = nil;
+ }
+}
+
+- (void)valdi_setMaximumDateSeconds:(id)value {
+ if ([value isKindOfClass:[NSNumber class]]) {
+ self.maxDate = [NSDate dateWithTimeIntervalSince1970:[(NSNumber *)value doubleValue]];
+ } else {
+ self.maxDate = nil;
+ }
+}
+
+- (void)valdi_setOnChange:(id)value {
+ _onChange = value;
+}
+
++ (void)bindAttributes:(SCValdiMacOSAttributesBinder *)attributesBinder {
+ [attributesBinder bindUntypedAttribute:@"dateSeconds"
+ invalidateLayoutOnChange:NO
+ selector:@selector(valdi_setDateSeconds:)];
+ [attributesBinder bindUntypedAttribute:@"minimumDateSeconds"
+ invalidateLayoutOnChange:NO
+ selector:@selector(valdi_setMinimumDateSeconds:)];
+ [attributesBinder bindUntypedAttribute:@"maximumDateSeconds"
+ invalidateLayoutOnChange:NO
+ selector:@selector(valdi_setMaximumDateSeconds:)];
+ [attributesBinder bindUntypedAttribute:@"onChange"
+ invalidateLayoutOnChange:NO
+ selector:@selector(valdi_setOnChange:)];
+}
+
+@end
diff --git a/valdi_modules/widgets/macos/SCWidgetsMacOSIndexPicker.h b/valdi_modules/widgets/macos/SCWidgetsMacOSIndexPicker.h
new file mode 100644
index 0000000..cb8a2b1
--- /dev/null
+++ b/valdi_modules/widgets/macos/SCWidgetsMacOSIndexPicker.h
@@ -0,0 +1,17 @@
+//
+// SCWidgetsMacOSIndexPicker.h
+// valdi-widgets-macos
+//
+
+#import "valdi/macos/SCValdiMacOSAttributesBinder.h"
+#import
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface SCWidgetsMacOSIndexPicker : NSPopUpButton
+
++ (void)bindAttributes:(SCValdiMacOSAttributesBinder *)attributesBinder;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/valdi_modules/widgets/macos/SCWidgetsMacOSIndexPicker.m b/valdi_modules/widgets/macos/SCWidgetsMacOSIndexPicker.m
new file mode 100644
index 0000000..108ac50
--- /dev/null
+++ b/valdi_modules/widgets/macos/SCWidgetsMacOSIndexPicker.m
@@ -0,0 +1,89 @@
+//
+// SCWidgetsMacOSIndexPicker.m
+// valdi-widgets-macos
+//
+
+#import "SCWidgetsMacOSIndexPicker.h"
+#import "valdi/macos/SCValdiMacOSFunction.h"
+
+@implementation SCWidgetsMacOSIndexPicker {
+ NSArray *_labels;
+ NSInteger _selectedIndex;
+ SCValdiMacOSFunction *_onChange;
+}
+
+- (instancetype)initWithFrame:(NSRect)frameRect {
+ self = [super initWithFrame:frameRect pullsDown:NO];
+ if (self) {
+ _labels = @[];
+ _selectedIndex = 0;
+ [self setTarget:self];
+ [self setAction:@selector(_handleOnChange)];
+ }
+ return self;
+}
+
+- (BOOL)acceptsFirstResponder {
+ return YES;
+}
+
+- (void)mouseDown:(NSEvent *)event {
+ // NSPopUpButton's built-in tracking doesn't work when embedded in a Valdi layer-backed hierarchy.
+ // Convert to screen coordinates and show the menu with inView:nil to bypass coordinate issues.
+ NSPoint windowPoint = [self convertPoint:NSZeroPoint toView:nil];
+ NSPoint screenPoint = [self.window convertPointToScreen:windowPoint];
+ [self.menu popUpMenuPositioningItem:self.selectedItem
+ atLocation:screenPoint
+ inView:nil];
+}
+
+- (void)_handleOnChange {
+ if (!_onChange) return;
+ NSInteger index = self.indexOfSelectedItem;
+ [_onChange performWithParameters:@[@(index)]];
+}
+
+- (void)valdi_setLabels:(id)value {
+ if (![value isKindOfClass:[NSArray class]]) return;
+ NSArray *newLabels = (NSArray *)value;
+ if ([newLabels isEqual:_labels]) return;
+ _labels = newLabels;
+ [self removeAllItems];
+ for (id label in _labels) {
+ if ([label isKindOfClass:[NSString class]]) {
+ [self addItemWithTitle:label];
+ }
+ }
+ NSInteger clamped = MAX(0, MIN(_selectedIndex, (NSInteger)_labels.count - 1));
+ if (_labels.count > 0) {
+ [self selectItemAtIndex:clamped];
+ }
+}
+
+- (void)valdi_setIndex:(id)value {
+ if (![value isKindOfClass:[NSNumber class]]) return;
+ NSInteger newIndex = [(NSNumber *)value integerValue];
+ newIndex = MAX(0, MIN(newIndex, (NSInteger)_labels.count - 1));
+ _selectedIndex = newIndex;
+ if (_labels.count > 0 && newIndex != self.indexOfSelectedItem) {
+ [self selectItemAtIndex:newIndex];
+ }
+}
+
+- (void)valdi_setOnChange:(id)value {
+ _onChange = value;
+}
+
++ (void)bindAttributes:(SCValdiMacOSAttributesBinder *)attributesBinder {
+ [attributesBinder bindUntypedAttribute:@"labels"
+ invalidateLayoutOnChange:YES
+ selector:@selector(valdi_setLabels:)];
+ [attributesBinder bindUntypedAttribute:@"index"
+ invalidateLayoutOnChange:NO
+ selector:@selector(valdi_setIndex:)];
+ [attributesBinder bindUntypedAttribute:@"onChange"
+ invalidateLayoutOnChange:NO
+ selector:@selector(valdi_setOnChange:)];
+}
+
+@end
diff --git a/valdi_modules/widgets/macos/SCWidgetsMacOSLabel.h b/valdi_modules/widgets/macos/SCWidgetsMacOSLabel.h
new file mode 100644
index 0000000..b0908f2
--- /dev/null
+++ b/valdi_modules/widgets/macos/SCWidgetsMacOSLabel.h
@@ -0,0 +1,18 @@
+//
+// SCWidgetsMacOSLabel.h
+// valdi-widgets-macos
+//
+
+#import "valdi/macos/SCValdiMacOSAttributesBinder.h"
+#import
+
+NS_ASSUME_NONNULL_BEGIN
+
+/// Non-editable NSTextField label that supports emoji rendering via AppKit.
+@interface SCWidgetsMacOSLabel : NSTextField
+
++ (void)bindAttributes:(SCValdiMacOSAttributesBinder *)attributesBinder;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/valdi_modules/widgets/macos/SCWidgetsMacOSLabel.m b/valdi_modules/widgets/macos/SCWidgetsMacOSLabel.m
new file mode 100644
index 0000000..2dfe12a
--- /dev/null
+++ b/valdi_modules/widgets/macos/SCWidgetsMacOSLabel.m
@@ -0,0 +1,82 @@
+//
+// SCWidgetsMacOSLabel.m
+// valdi-widgets-macos
+//
+
+#import "SCWidgetsMacOSLabel.h"
+
+static NSFont *SCWidgetsMacOSResolveFont(NSString *str) {
+ if (![str isKindOfClass:[NSString class]]) return nil;
+ NSArray *parts = [str componentsSeparatedByString:@" "];
+ if (parts.count < 2) return nil;
+ NSString *name = parts[0];
+ CGFloat size = [parts[1] doubleValue];
+ if ([name isEqualToString:@"system-bold"]) {
+ return [NSFont boldSystemFontOfSize:size];
+ }
+ if ([name isEqualToString:@"system"]) {
+ return [NSFont systemFontOfSize:size];
+ }
+ return [NSFont fontWithName:name size:size];
+}
+
+static NSColor *SCWidgetsMacOSResolveColor(NSNumber *color) {
+ if (![color isKindOfClass:[NSNumber class]]) return nil;
+ long value = [color longValue];
+ CGFloat r = ((value >> 24) & 0xFF) / 255.0;
+ CGFloat g = ((value >> 16) & 0xFF) / 255.0;
+ CGFloat b = ((value >> 8) & 0xFF) / 255.0;
+ CGFloat a = (value & 0xFF) / 255.0;
+ return [NSColor colorWithRed:r green:g blue:b alpha:a];
+}
+
+@implementation SCWidgetsMacOSLabel
+
+- (instancetype)initWithFrame:(NSRect)frameRect {
+ self = [super initWithFrame:frameRect];
+ if (self) {
+ self.editable = NO;
+ self.bezeled = NO;
+ self.drawsBackground = NO;
+ self.selectable = NO;
+ self.lineBreakMode = NSLineBreakByWordWrapping;
+ }
+ return self;
+}
+
+- (void)valdi_setText:(id)value {
+ self.stringValue = [value isKindOfClass:[NSString class]] ? value : @"";
+}
+
+- (void)valdi_setColor:(id)value {
+ NSColor *color = SCWidgetsMacOSResolveColor(value);
+ self.textColor = color ?: [NSColor labelColor];
+}
+
+- (void)valdi_setFont:(id)value {
+ NSFont *font = SCWidgetsMacOSResolveFont(value);
+ self.font = font ?: [NSFont systemFontOfSize:[NSFont systemFontSize]];
+}
+
+- (void)valdi_setNumberOfLines:(id)value {
+ if ([value isKindOfClass:[NSNumber class]]) {
+ self.maximumNumberOfLines = [(NSNumber *)value integerValue];
+ }
+}
+
++ (void)bindAttributes:(SCValdiMacOSAttributesBinder *)attributesBinder {
+ [attributesBinder bindUntypedAttribute:@"value"
+ invalidateLayoutOnChange:YES
+ selector:@selector(valdi_setText:)];
+ [attributesBinder bindColorAttribute:@"color"
+ invalidateLayoutOnChange:NO
+ selector:@selector(valdi_setColor:)];
+ [attributesBinder bindUntypedAttribute:@"font"
+ invalidateLayoutOnChange:YES
+ selector:@selector(valdi_setFont:)];
+ [attributesBinder bindUntypedAttribute:@"numberOfLines"
+ invalidateLayoutOnChange:YES
+ selector:@selector(valdi_setNumberOfLines:)];
+}
+
+@end
diff --git a/valdi_modules/widgets/macos/SCWidgetsMacOSTimePicker.h b/valdi_modules/widgets/macos/SCWidgetsMacOSTimePicker.h
new file mode 100644
index 0000000..5791e4c
--- /dev/null
+++ b/valdi_modules/widgets/macos/SCWidgetsMacOSTimePicker.h
@@ -0,0 +1,17 @@
+//
+// SCWidgetsMacOSTimePicker.h
+// valdi-widgets-macos
+//
+
+#import "valdi/macos/SCValdiMacOSAttributesBinder.h"
+#import
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface SCWidgetsMacOSTimePicker : NSDatePicker
+
++ (void)bindAttributes:(SCValdiMacOSAttributesBinder *)attributesBinder;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/valdi_modules/widgets/macos/SCWidgetsMacOSTimePicker.m b/valdi_modules/widgets/macos/SCWidgetsMacOSTimePicker.m
new file mode 100644
index 0000000..e3cc0d3
--- /dev/null
+++ b/valdi_modules/widgets/macos/SCWidgetsMacOSTimePicker.m
@@ -0,0 +1,98 @@
+//
+// SCWidgetsMacOSTimePicker.m
+// valdi-widgets-macos
+//
+
+#import "SCWidgetsMacOSTimePicker.h"
+#import "valdi/macos/SCValdiMacOSFunction.h"
+
+@implementation SCWidgetsMacOSTimePicker {
+ SCValdiMacOSFunction *_onChange;
+ // Track pending hour/minute updates so we can apply both atomically.
+ NSInteger _pendingHour;
+ NSInteger _pendingMinute;
+ BOOL _hasPendingHour;
+ BOOL _hasPendingMinute;
+}
+
+- (instancetype)initWithFrame:(NSRect)frameRect {
+ self = [super initWithFrame:frameRect];
+ if (self) {
+ self.datePickerElements = NSDatePickerElementFlagHourMinute;
+ self.datePickerStyle = NSDatePickerStyleTextField;
+ self.target = self;
+ self.action = @selector(_handleOnChange);
+ _pendingHour = 0;
+ _pendingMinute = 0;
+ _hasPendingHour = NO;
+ _hasPendingMinute = NO;
+ }
+ return self;
+}
+
+- (void)_handleOnChange {
+ if (!_onChange) return;
+ NSCalendar *calendar = [NSCalendar calendarWithIdentifier:NSCalendarIdentifierGregorian];
+ NSDateComponents *comps = [calendar components:(NSCalendarUnitHour | NSCalendarUnitMinute)
+ fromDate:self.dateValue];
+ [_onChange performWithParameters:@[@{@"hourOfDay": @(comps.hour), @"minuteOfHour": @(comps.minute)}]];
+}
+
+- (void)_applyPendingTimeIfReady {
+ if (!_hasPendingHour || !_hasPendingMinute) return;
+ _hasPendingHour = NO;
+ _hasPendingMinute = NO;
+ NSCalendar *calendar = [NSCalendar calendarWithIdentifier:NSCalendarIdentifierGregorian];
+ NSDateComponents *comps = [[NSDateComponents alloc] init];
+ comps.hour = _pendingHour;
+ comps.minute = _pendingMinute;
+ // Use a reference date to avoid date pollution in time-only mode
+ comps.year = 2000;
+ comps.month = 1;
+ comps.day = 1;
+ NSDate *date = [calendar dateFromComponents:comps];
+ if (date) {
+ self.dateValue = date;
+ }
+}
+
+- (void)valdi_setHourOfDay:(id)value {
+ if ([value isKindOfClass:[NSNumber class]]) {
+ _pendingHour = [(NSNumber *)value integerValue];
+ _hasPendingHour = YES;
+ [self _applyPendingTimeIfReady];
+ }
+}
+
+- (void)valdi_setMinuteOfHour:(id)value {
+ if ([value isKindOfClass:[NSNumber class]]) {
+ _pendingMinute = [(NSNumber *)value integerValue];
+ _hasPendingMinute = YES;
+ [self _applyPendingTimeIfReady];
+ }
+}
+
+- (void)valdi_setIntervalMinutes:(id)__unused value {
+ // NSDatePicker on macOS does not support custom minute intervals.
+}
+
+- (void)valdi_setOnChange:(id)value {
+ _onChange = value;
+}
+
++ (void)bindAttributes:(SCValdiMacOSAttributesBinder *)attributesBinder {
+ [attributesBinder bindUntypedAttribute:@"hourOfDay"
+ invalidateLayoutOnChange:NO
+ selector:@selector(valdi_setHourOfDay:)];
+ [attributesBinder bindUntypedAttribute:@"minuteOfHour"
+ invalidateLayoutOnChange:NO
+ selector:@selector(valdi_setMinuteOfHour:)];
+ [attributesBinder bindUntypedAttribute:@"intervalMinutes"
+ invalidateLayoutOnChange:NO
+ selector:@selector(valdi_setIntervalMinutes:)];
+ [attributesBinder bindUntypedAttribute:@"onChange"
+ invalidateLayoutOnChange:NO
+ selector:@selector(valdi_setOnChange:)];
+}
+
+@end
diff --git a/valdi_modules/widgets/src/components/button/FloatingActionButton.tsx b/valdi_modules/widgets/src/components/button/FloatingActionButton.tsx
index b8e327f..81ae789 100644
--- a/valdi_modules/widgets/src/components/button/FloatingActionButton.tsx
+++ b/valdi_modules/widgets/src/components/button/FloatingActionButton.tsx
@@ -7,7 +7,7 @@ import { Layout } from 'valdi_tsx/src/NativeTemplateElements';
import res from 'widgets/res';
import { isCustomTheme } from 'widgets/src/InitSemanticColors';
import { Theme, ThemeType } from 'widgets/src/Theme';
-import { SemanticColor } from '../../styles/semanticColors';
+import { SemanticColor } from 'widgets/src/styles/semanticColors';
import { CoreButton, CoreButtonSizing, sizingOptions } from './CoreButton';
export interface FloatingActionButtonViewModel {
diff --git a/valdi_modules/widgets/src/components/card/CollapsibleCard.tsx b/valdi_modules/widgets/src/components/card/CollapsibleCard.tsx
index 357fe94..7913cb5 100644
--- a/valdi_modules/widgets/src/components/card/CollapsibleCard.tsx
+++ b/valdi_modules/widgets/src/components/card/CollapsibleCard.tsx
@@ -11,8 +11,8 @@ import { HorizontalRule } from 'widgets/src/components/rules/HorizontalRule';
import { TextStyleFont } from 'widgets/src/styles/TextStyleFont';
import { SemanticColor } from 'widgets/src/styles/semanticColors';
import { Spacing } from 'widgets/src/styles/spacing';
-import { RenderFunction } from '../RenderFunction';
-import { ScrollViewHandler } from '../scroll/ScrollViewHandler';
+import { RenderFunction } from 'widgets/src/components/RenderFunction';
+import { ScrollViewHandler } from 'widgets/src/components/scroll/ScrollViewHandler';
export interface CollapsibleCardRenderFunction {
renderFunction: RenderFunction;
diff --git a/valdi_modules/widgets/src/components/cell/subtitle/DelimitedCellSubtitle.tsx b/valdi_modules/widgets/src/components/cell/subtitle/DelimitedCellSubtitle.tsx
index 8e3a1af..3fb625e 100644
--- a/valdi_modules/widgets/src/components/cell/subtitle/DelimitedCellSubtitle.tsx
+++ b/valdi_modules/widgets/src/components/cell/subtitle/DelimitedCellSubtitle.tsx
@@ -1,7 +1,7 @@
import { Component } from 'valdi_core/src/Component';
import { when } from 'valdi_core/src/utils/When';
import { SemanticColor } from 'widgets/src/styles/semanticColors';
-import { RenderFunction } from '../../RenderFunction';
+import { RenderFunction } from 'widgets/src/components/RenderFunction';
import { CellPacking, packingSubtitleMinHeights, style, SUBTITLE_DELIMITER } from '../style';
export interface GenericCellSubtitleViewModel {
diff --git a/valdi_modules/widgets/src/components/cell/subtitle/component/SubtitleBrand.tsx b/valdi_modules/widgets/src/components/cell/subtitle/component/SubtitleBrand.tsx
index b93a98c..f910f17 100644
--- a/valdi_modules/widgets/src/components/cell/subtitle/component/SubtitleBrand.tsx
+++ b/valdi_modules/widgets/src/components/cell/subtitle/component/SubtitleBrand.tsx
@@ -1,7 +1,7 @@
import { Component } from 'valdi_core/src/Component';
import { CommonLabel, ViewAttributes } from 'valdi_tsx/src/NativeTemplateElements';
import { TextStyleFont } from 'widgets/src/styles/TextStyleFont';
-import { SemanticColor } from '../../../../styles/semanticColors';
+import { SemanticColor } from 'widgets/src/styles/semanticColors';
import { SubtitleText } from './SubtitleText';
export class SubtitleBrand extends Component {
diff --git a/valdi_modules/widgets/src/components/indexview/IndexView.tsx b/valdi_modules/widgets/src/components/indexview/IndexView.tsx
index 2dde056..30304f4 100644
--- a/valdi_modules/widgets/src/components/indexview/IndexView.tsx
+++ b/valdi_modules/widgets/src/components/indexview/IndexView.tsx
@@ -10,7 +10,7 @@ import { TextStyleFontNonDynamic } from 'widgets/src/styles/TextStyleFont';
import { SemanticColor } from 'widgets/src/styles/semanticColors';
import { Spacing } from 'widgets/src/styles/spacing';
import { clamp } from 'foundation/src/number';
-import { ScrollViewHandler } from '../scroll/ScrollViewHandler';
+import { ScrollViewHandler } from 'widgets/src/components/scroll/ScrollViewHandler';
import { IndexViewHandler } from './IndexViewHandler';
import { IndexViewSymbol } from './IndexViewSymbol';
diff --git a/valdi_modules/widgets/src/components/inputs/CoreTextField.tsx b/valdi_modules/widgets/src/components/inputs/CoreTextField.tsx
index c1725f9..f502477 100644
--- a/valdi_modules/widgets/src/components/inputs/CoreTextField.tsx
+++ b/valdi_modules/widgets/src/components/inputs/CoreTextField.tsx
@@ -13,7 +13,7 @@ import res from 'widgets/res';
import { TextStyleFont } from 'widgets/src/styles/TextStyleFont';
import { SemanticColor } from 'widgets/src/styles/semanticColors';
import { Spacing } from 'widgets/src/styles/spacing';
-import { FocusableComponentRef } from '../util/FocusableComponentRef';
+import { FocusableComponentRef } from 'widgets/src/components/util/FocusableComponentRef';
import { ScrollWithKeyboard } from './ScrollWithKeyboard';
import { TextFieldWrapper } from './TextFieldWrapper';
diff --git a/valdi_modules/widgets/src/components/inputs/FloatLabeledTextField.tsx b/valdi_modules/widgets/src/components/inputs/FloatLabeledTextField.tsx
index 3ff92a0..d668c99 100644
--- a/valdi_modules/widgets/src/components/inputs/FloatLabeledTextField.tsx
+++ b/valdi_modules/widgets/src/components/inputs/FloatLabeledTextField.tsx
@@ -11,7 +11,7 @@ import {
import { TextStyleFont } from 'widgets/src/styles/TextStyleFont';
import { SemanticColor } from 'widgets/src/styles/semanticColors';
import { Spacing } from 'widgets/src/styles/spacing';
-import { FocusableComponentRef } from '../util/FocusableComponentRef';
+import { FocusableComponentRef } from 'widgets/src/components/util/FocusableComponentRef';
import { ScrollWithKeyboard } from './ScrollWithKeyboard';
import { TextFieldWrapper } from './TextFieldWrapper';
diff --git a/valdi_modules/widgets/src/components/inputs/FormTextView.tsx b/valdi_modules/widgets/src/components/inputs/FormTextView.tsx
index 4aaeae0..f93182d 100644
--- a/valdi_modules/widgets/src/components/inputs/FormTextView.tsx
+++ b/valdi_modules/widgets/src/components/inputs/FormTextView.tsx
@@ -10,7 +10,7 @@ import {
} from 'valdi_tsx/src/NativeTemplateElements';
import { TextStyleFont } from 'widgets/src/styles/TextStyleFont';
import { SemanticColor } from 'widgets/src/styles/semanticColors';
-import { FocusableComponentRef } from '../util/FocusableComponentRef';
+import { FocusableComponentRef } from 'widgets/src/components/util/FocusableComponentRef';
import { ScrollWithKeyboard } from './ScrollWithKeyboard';
import { TextViewWrapper } from './TextViewWrapper';
diff --git a/valdi_modules/widgets/src/components/inputs/PillTextField.tsx b/valdi_modules/widgets/src/components/inputs/PillTextField.tsx
index 7bfb2ab..983fbef 100644
--- a/valdi_modules/widgets/src/components/inputs/PillTextField.tsx
+++ b/valdi_modules/widgets/src/components/inputs/PillTextField.tsx
@@ -19,8 +19,8 @@ import res from 'widgets/res';
import { TextStyleFont } from 'widgets/src/styles/TextStyleFont';
import { SemanticColor } from 'widgets/src/styles/semanticColors';
import { Spacing } from 'widgets/src/styles/spacing';
-import { ScrollViewHandler } from '../scroll/ScrollViewHandler';
-import { FocusableComponentRef } from '../util/FocusableComponentRef';
+import { ScrollViewHandler } from 'widgets/src/components/scroll/ScrollViewHandler';
+import { FocusableComponentRef } from 'widgets/src/components/util/FocusableComponentRef';
import { ScrollWithKeyboard } from './ScrollWithKeyboard';
import { TextFieldWrapper } from './TextFieldWrapper';
diff --git a/valdi_modules/widgets/src/components/inputs/ScrollWithKeyboard.d.ts b/valdi_modules/widgets/src/components/inputs/ScrollWithKeyboard.d.ts
index 2fb88b2..8924c89 100644
--- a/valdi_modules/widgets/src/components/inputs/ScrollWithKeyboard.d.ts
+++ b/valdi_modules/widgets/src/components/inputs/ScrollWithKeyboard.d.ts
@@ -1,4 +1,4 @@
-import { ScrollViewHandler } from '../scroll/ScrollViewHandler';
+import { ScrollViewHandler } from 'widgets/src/components/scroll/ScrollViewHandler';
/**
* Depending on context in which the valdi view is rendered, it may make sense to change the auto-scroll behaviour
diff --git a/valdi_modules/widgets/src/components/inputs/TextInputWrapper.tsx b/valdi_modules/widgets/src/components/inputs/TextInputWrapper.tsx
index 2588846..bd733f4 100644
--- a/valdi_modules/widgets/src/components/inputs/TextInputWrapper.tsx
+++ b/valdi_modules/widgets/src/components/inputs/TextInputWrapper.tsx
@@ -6,8 +6,8 @@ import { RenderedElementUtils } from 'valdi_core/src/utils/RenderedElementUtils'
import { ElementFrame } from 'valdi_tsx/src/Geometry';
import { EditTextBeginEvent, EditTextEndEvent } from 'valdi_tsx/src/NativeTemplateElements';
import { Spacing } from 'widgets/src/styles/spacing';
-import { ScrollViewHandler } from '../scroll/ScrollViewHandler';
-import { ScrollViewSubscription } from '../scroll/ScrollViewSubscription';
+import { ScrollViewHandler } from 'widgets/src/components/scroll/ScrollViewHandler';
+import { ScrollViewSubscription } from 'widgets/src/components/scroll/ScrollViewSubscription';
import { ScrollWithKeyboard, ScrollWithKeyboardMode } from './ScrollWithKeyboard';
import { TextInputHandler } from './TextInputHandler';
diff --git a/valdi_modules/widgets/src/components/pickers/DatePicker.tsx b/valdi_modules/widgets/src/components/pickers/DatePicker.tsx
index 19ea942..22327fd 100644
--- a/valdi_modules/widgets/src/components/pickers/DatePicker.tsx
+++ b/valdi_modules/widgets/src/components/pickers/DatePicker.tsx
@@ -32,8 +32,10 @@ export class DatePicker extends Component {
onRender(): void {
;
diff --git a/valdi_modules/widgets/src/components/pickers/IndexPicker.tsx b/valdi_modules/widgets/src/components/pickers/IndexPicker.tsx
index d328075..aa202a7 100644
--- a/valdi_modules/widgets/src/components/pickers/IndexPicker.tsx
+++ b/valdi_modules/widgets/src/components/pickers/IndexPicker.tsx
@@ -17,8 +17,10 @@ export class IndexPicker extends Component {
const labels = viewModel.labels;
const index = clamp(viewModel.index ?? 0, 0, labels.length - 1);
{
onRender(): void {
;
diff --git a/valdi_modules/widgets/src/components/refresh/DataSyncingBar.tsx b/valdi_modules/widgets/src/components/refresh/DataSyncingBar.tsx
index 06ce699..901fc23 100644
--- a/valdi_modules/widgets/src/components/refresh/DataSyncingBar.tsx
+++ b/valdi_modules/widgets/src/components/refresh/DataSyncingBar.tsx
@@ -4,7 +4,7 @@ import { ElementFrame } from 'valdi_tsx/src/Geometry';
import { View } from 'valdi_tsx/src/NativeTemplateElements';
import { GradientDirection, linearGradient } from 'widgets/src/styles/gradients';
import { SemanticColor } from 'widgets/src/styles/semanticColorsGen';
-import { ScrollViewHandler } from '../scroll/ScrollViewHandler';
+import { ScrollViewHandler } from 'widgets/src/components/scroll/ScrollViewHandler';
interface DataSyncingBarState {
width: number;
diff --git a/valdi_modules/widgets/src/components/refresh/PullToRefresh.tsx b/valdi_modules/widgets/src/components/refresh/PullToRefresh.tsx
index 0873860..be9b98e 100644
--- a/valdi_modules/widgets/src/components/refresh/PullToRefresh.tsx
+++ b/valdi_modules/widgets/src/components/refresh/PullToRefresh.tsx
@@ -10,9 +10,9 @@ import { ScrollViewSubscription } from 'widgets/src/components/scroll/ScrollView
import { linearGradient } from 'widgets/src/styles/gradients';
import { SemanticColor } from 'widgets/src/styles/semanticColors';
import { clamp } from 'foundation/src/number';
-import { Ids } from '../../../ids';
-import { EasingFunctions } from '../animation/EasingFunctions';
-import { ScrollViewListener } from '../scroll/ScrollViewListener';
+import { Ids } from 'widgets/ids';
+import { EasingFunctions } from 'widgets/src/components/animation/EasingFunctions';
+import { ScrollViewListener } from 'widgets/src/components/scroll/ScrollViewListener';
import { DataSyncingBar } from './DataSyncingBar';
/**
diff --git a/valdi_modules/widgets/src/components/section/SectionHeader.tsx b/valdi_modules/widgets/src/components/section/SectionHeader.tsx
index dcb2b81..0f04415 100644
--- a/valdi_modules/widgets/src/components/section/SectionHeader.tsx
+++ b/valdi_modules/widgets/src/components/section/SectionHeader.tsx
@@ -7,7 +7,7 @@ import { Subscreen } from 'widgets/src/components/subscreen/Subscreen';
import { TextStyleFont } from 'widgets/src/styles/TextStyleFont';
import { SemanticColor } from 'widgets/src/styles/semanticColors';
import { Spacing } from 'widgets/src/styles/spacing';
-import { RenderFunction } from '../RenderFunction';
+import { RenderFunction } from 'widgets/src/components/RenderFunction';
import { SectionHeaderActionButton } from './header/SectionHeaderActionButton';
export interface ISectionActionButton {
diff --git a/valdi_modules/widgets/src/components/section/SectionList.tsx b/valdi_modules/widgets/src/components/section/SectionList.tsx
index 7e49800..bca0e4c 100644
--- a/valdi_modules/widgets/src/components/section/SectionList.tsx
+++ b/valdi_modules/widgets/src/components/section/SectionList.tsx
@@ -12,7 +12,7 @@ import { linearGradient } from 'widgets/src/styles/gradients';
import { SemanticColor } from 'widgets/src/styles/semanticColors';
import { binarySearch } from 'coreutils/src/ArrayUtils';
import { clamp } from 'foundation/src/number';
-import { ScrollViewSubscription } from '../scroll/ScrollViewSubscription';
+import { ScrollViewSubscription } from 'widgets/src/components/scroll/ScrollViewSubscription';
import { SectionHandler } from './SectionHandler';
import { SectionModel } from './SectionModel';
import { SectionSeparator } from './SectionSeparator';
diff --git a/valdi_modules/widgets/src/components/section/SectionModel.d.ts b/valdi_modules/widgets/src/components/section/SectionModel.d.ts
index 5fe12ae..a092306 100644
--- a/valdi_modules/widgets/src/components/section/SectionModel.d.ts
+++ b/valdi_modules/widgets/src/components/section/SectionModel.d.ts
@@ -1,4 +1,4 @@
-import { RenderFunction } from '../RenderFunction';
+import { RenderFunction } from 'widgets/src/components/RenderFunction';
import { SectionHandler } from './SectionHandler';
export type RenderFunctionAnchor = RenderFunction;
diff --git a/valdi_modules/widgets/src/components/section/header/SectionHeaderActionButton.tsx b/valdi_modules/widgets/src/components/section/header/SectionHeaderActionButton.tsx
index b919c6f..7b2ac16 100644
--- a/valdi_modules/widgets/src/components/section/header/SectionHeaderActionButton.tsx
+++ b/valdi_modules/widgets/src/components/section/header/SectionHeaderActionButton.tsx
@@ -6,7 +6,7 @@ import { Label, View, ImageView } from 'valdi_tsx/src/NativeTemplateElements';
import res from 'widgets/res';
import { CoreButton, CoreButtonColoring, CoreButtonSizing } from 'widgets/src/components/button/CoreButton';
import { TextStyleFont } from 'widgets/src/styles/TextStyleFont';
-import { SemanticColor } from '../../../styles/semanticColors';
+import { SemanticColor } from 'widgets/src/styles/semanticColors';
export interface SectionHeaderActionButtonViewModel {
/**
diff --git a/valdi_modules/widgets/src/components/section/header/SectionHeaderLabel.tsx b/valdi_modules/widgets/src/components/section/header/SectionHeaderLabel.tsx
index f5b11b0..56ecf64 100644
--- a/valdi_modules/widgets/src/components/section/header/SectionHeaderLabel.tsx
+++ b/valdi_modules/widgets/src/components/section/header/SectionHeaderLabel.tsx
@@ -2,7 +2,7 @@ import { Component } from 'valdi_core/src/Component';
import { Style } from 'valdi_core/src/Style';
import { Label, Layout, ImageView } from 'valdi_tsx/src/NativeTemplateElements';
import { TextStyleFont } from 'widgets/src/styles/TextStyleFont';
-import { SemanticColor } from '../../../styles/semanticColors';
+import { SemanticColor } from 'widgets/src/styles/semanticColors';
export interface SectionHeaderLabelViewModel {
text: string;
diff --git a/valdi_modules/widgets/src/components/section/utils/makeSectionModelSimple.tsx b/valdi_modules/widgets/src/components/section/utils/makeSectionModelSimple.tsx
index 74fd2c0..ddf7b4f 100644
--- a/valdi_modules/widgets/src/components/section/utils/makeSectionModelSimple.tsx
+++ b/valdi_modules/widgets/src/components/section/utils/makeSectionModelSimple.tsx
@@ -1,5 +1,5 @@
-import { RenderFunction } from '../../RenderFunction';
-import { Card } from '../../card/Card';
+import { RenderFunction } from 'widgets/src/components/RenderFunction';
+import { Card } from 'widgets/src/components/card/Card';
import { SectionBody } from '../SectionBody';
import { SectionHeader } from '../SectionHeader';
import { SectionModel } from '../SectionModel';
diff --git a/valdi_modules/widgets/src/components/subscreen/Subscreen.tsx b/valdi_modules/widgets/src/components/subscreen/Subscreen.tsx
index 8e50a5c..a39c8eb 100644
--- a/valdi_modules/widgets/src/components/subscreen/Subscreen.tsx
+++ b/valdi_modules/widgets/src/components/subscreen/Subscreen.tsx
@@ -13,10 +13,10 @@ import { ScrollViewHandler } from 'widgets/src/components/scroll/ScrollViewHandl
import { ScrollViewListener, ScrollViewRegionEvent } from 'widgets/src/components/scroll/ScrollViewListener';
import { SemanticColor } from 'widgets/src/styles/semanticColors';
import { clamp } from 'foundation/src/number';
-import { FadeImage } from '../image/FadeImage';
-import { ScrollViewSubscription } from '../scroll/ScrollViewSubscription';
-import { ScrollBar } from '../scroll/scrollbar/ScrollBar';
-import { ScrollBarHandler } from '../scroll/scrollbar/ScrollBarHandler';
+import { FadeImage } from 'widgets/src/components/image/FadeImage';
+import { ScrollViewSubscription } from 'widgets/src/components/scroll/ScrollViewSubscription';
+import { ScrollBar } from 'widgets/src/components/scroll/scrollbar/ScrollBar';
+import { ScrollBarHandler } from 'widgets/src/components/scroll/scrollbar/ScrollBarHandler';
const SCROLLBAR_SIDE_OFFSET = 9;
diff --git a/valdi_modules/widgets/src/components/subscreen/SubscreenContent.tsx b/valdi_modules/widgets/src/components/subscreen/SubscreenContent.tsx
index 64dec60..960e401 100644
--- a/valdi_modules/widgets/src/components/subscreen/SubscreenContent.tsx
+++ b/valdi_modules/widgets/src/components/subscreen/SubscreenContent.tsx
@@ -1,6 +1,6 @@
import { Component } from 'valdi_core/src/Component';
-import { BLANK_INSETS, Insets } from '../util/Insets';
-import { WithInsets } from '../util/WithInsets';
+import { BLANK_INSETS, Insets } from 'widgets/src/components/util/Insets';
+import { WithInsets } from 'widgets/src/components/util/WithInsets';
import { Subscreen } from './Subscreen';
export interface SubscreenContentViewModel {
diff --git a/valdi_modules/widgets/src/components/subscreen/SubscreenHeader.tsx b/valdi_modules/widgets/src/components/subscreen/SubscreenHeader.tsx
index c69ee8c..8c4f7e1 100644
--- a/valdi_modules/widgets/src/components/subscreen/SubscreenHeader.tsx
+++ b/valdi_modules/widgets/src/components/subscreen/SubscreenHeader.tsx
@@ -1,7 +1,7 @@
import { Component } from 'valdi_core/src/Component';
import { Device } from 'valdi_core/src/Device';
-import { BLANK_INSETS, Insets } from '../util/Insets';
-import { WithInsets } from '../util/WithInsets';
+import { BLANK_INSETS, Insets } from 'widgets/src/components/util/Insets';
+import { WithInsets } from 'widgets/src/components/util/WithInsets';
import { Subscreen } from './Subscreen';
/**
diff --git a/valdi_modules/widgets/src/components/subscreen/SubscreenSections.tsx b/valdi_modules/widgets/src/components/subscreen/SubscreenSections.tsx
index b1d14e3..7c9bdac 100644
--- a/valdi_modules/widgets/src/components/subscreen/SubscreenSections.tsx
+++ b/valdi_modules/widgets/src/components/subscreen/SubscreenSections.tsx
@@ -7,12 +7,12 @@ import { ScrollViewHandler } from 'widgets/src/components/scroll/ScrollViewHandl
import { SectionList } from 'widgets/src/components/section/SectionList';
import { SectionModel } from 'widgets/src/components/section/SectionModel';
import { SemanticColor } from 'widgets/src/styles/semanticColors';
-import { RenderFunction } from '../RenderFunction';
-import { IndexViewAnchor } from '../indexview/IndexViewAnchor';
-import { IndexViewSymbol } from '../indexview/IndexViewSymbol';
-import { lookupSymbolForString } from '../indexview/utils/lookupSymbolForString';
-import { PullToRefresh, PullToRefreshEvent } from '../refresh/PullToRefresh';
-import { ScrollBarHandler } from '../scroll/scrollbar/ScrollBarHandler';
+import { RenderFunction } from 'widgets/src/components/RenderFunction';
+import { IndexViewAnchor } from 'widgets/src/components/indexview/IndexViewAnchor';
+import { IndexViewSymbol } from 'widgets/src/components/indexview/IndexViewSymbol';
+import { lookupSymbolForString } from 'widgets/src/components/indexview/utils/lookupSymbolForString';
+import { PullToRefresh, PullToRefreshEvent } from 'widgets/src/components/refresh/PullToRefresh';
+import { ScrollBarHandler } from 'widgets/src/components/scroll/scrollbar/ScrollBarHandler';
import { Subscreen } from './Subscreen';
import { SubscreenContent } from './SubscreenContent';
import { SubscreenHeader } from './SubscreenHeader';
diff --git a/valdi_modules/widgets/src/components/subscreen/SubscreenTabs.tsx b/valdi_modules/widgets/src/components/subscreen/SubscreenTabs.tsx
index f3d9271..ce81509 100644
--- a/valdi_modules/widgets/src/components/subscreen/SubscreenTabs.tsx
+++ b/valdi_modules/widgets/src/components/subscreen/SubscreenTabs.tsx
@@ -2,12 +2,12 @@ import { StatefulComponent } from 'valdi_core/src/Component';
import { Subscription } from 'valdi_rxjs/src/Subscription';
import { ScrollViewHandler } from 'widgets/src/components/scroll/ScrollViewHandler';
import { SemanticColor } from 'widgets/src/styles/semanticColors';
-import { PullToRefresh, PullToRefreshEvent } from '../refresh/PullToRefresh';
-import { ScrollBarHandler } from '../scroll/scrollbar/ScrollBarHandler';
-import { TabsContent, TabsContentLoading } from '../tabs/TabsContent';
-import { TabsCoordinator } from '../tabs/TabsCoordinator';
-import { TabsHeader } from '../tabs/TabsHeader';
-import { TabsItem } from '../tabs/TabsItem';
+import { PullToRefresh, PullToRefreshEvent } from 'widgets/src/components/refresh/PullToRefresh';
+import { ScrollBarHandler } from 'widgets/src/components/scroll/scrollbar/ScrollBarHandler';
+import { TabsContent, TabsContentLoading } from 'widgets/src/components/tabs/TabsContent';
+import { TabsCoordinator } from 'widgets/src/components/tabs/TabsCoordinator';
+import { TabsHeader } from 'widgets/src/components/tabs/TabsHeader';
+import { TabsItem } from 'widgets/src/components/tabs/TabsItem';
import { Subscreen } from './Subscreen';
import { SubscreenContent } from './SubscreenContent';
import { SubscreenHeader, SubscreenHeaderPacking } from './SubscreenHeader';
diff --git a/valdi_modules/widgets/src/components/tabs/TabsContent.tsx b/valdi_modules/widgets/src/components/tabs/TabsContent.tsx
index f39c90b..70cf9b9 100644
--- a/valdi_modules/widgets/src/components/tabs/TabsContent.tsx
+++ b/valdi_modules/widgets/src/components/tabs/TabsContent.tsx
@@ -10,8 +10,8 @@ import { Layout, ScrollView, View } from 'valdi_tsx/src/NativeTemplateElements';
import { linearGradient } from 'widgets/src/styles/gradients';
import { SemanticColor } from 'widgets/src/styles/semanticColors';
import { clamp } from 'foundation/src/number';
-import { ScrollViewHandler } from '../scroll/ScrollViewHandler';
-import { ScrollViewSubscription } from '../scroll/ScrollViewSubscription';
+import { ScrollViewHandler } from 'widgets/src/components/scroll/ScrollViewHandler';
+import { ScrollViewSubscription } from 'widgets/src/components/scroll/ScrollViewSubscription';
import { TabsCoordinator, TabsCoordinatorItems } from './TabsCoordinator';
export enum TabsContentLoading {
@@ -159,15 +159,18 @@ export class TabsContent extends StatefulComponent
diff --git a/valdi_modules/widgets/src/components/tabs/TabsHeader.tsx b/valdi_modules/widgets/src/components/tabs/TabsHeader.tsx
index 8ec7c78..170c158 100644
--- a/valdi_modules/widgets/src/components/tabs/TabsHeader.tsx
+++ b/valdi_modules/widgets/src/components/tabs/TabsHeader.tsx
@@ -9,8 +9,8 @@ import { Layout, ScrollView, View } from 'valdi_tsx/src/NativeTemplateElements';
import { SemanticColor } from 'widgets/src/styles/semanticColors';
import { Spacing } from 'widgets/src/styles/spacing';
import { clamp, lerp } from 'foundation/src/number';
-import { ScrollViewHandler } from '../scroll/ScrollViewHandler';
-import { ScrollViewSubscription } from '../scroll/ScrollViewSubscription';
+import { ScrollViewHandler } from 'widgets/src/components/scroll/ScrollViewHandler';
+import { ScrollViewSubscription } from 'widgets/src/components/scroll/ScrollViewSubscription';
import { TabsCoordinator, TabsCoordinatorItems } from './TabsCoordinator';
import { TabsItem } from './TabsItem';
diff --git a/valdi_modules/widgets/src/components/tabs/TabsItem.tsx b/valdi_modules/widgets/src/components/tabs/TabsItem.tsx
index 131080d..9a51f9d 100644
--- a/valdi_modules/widgets/src/components/tabs/TabsItem.tsx
+++ b/valdi_modules/widgets/src/components/tabs/TabsItem.tsx
@@ -1,4 +1,4 @@
-import { ScrollBarHandler } from '../scroll/scrollbar/ScrollBarHandler';
+import { ScrollBarHandler } from 'widgets/src/components/scroll/scrollbar/ScrollBarHandler';
export interface TabsItem {
key?: string;
diff --git a/valdi_modules/widgets/src/components/tabs/TabsItemWithTitle.tsx b/valdi_modules/widgets/src/components/tabs/TabsItemWithTitle.tsx
index 267a554..d774736 100644
--- a/valdi_modules/widgets/src/components/tabs/TabsItemWithTitle.tsx
+++ b/valdi_modules/widgets/src/components/tabs/TabsItemWithTitle.tsx
@@ -5,7 +5,7 @@ import { Label, Layout, View } from 'valdi_tsx/src/NativeTemplateElements';
import { TextStyleFont } from 'widgets/src/styles/TextStyleFont';
import { SemanticColor } from 'widgets/src/styles/semanticColors';
import { Spacing } from 'widgets/src/styles/spacing';
-import { ScrollBarHandler } from '../scroll/scrollbar/ScrollBarHandler';
+import { ScrollBarHandler } from 'widgets/src/components/scroll/scrollbar/ScrollBarHandler';
import { TabsItem } from './TabsItem';
/**
diff --git a/valdi_modules/widgets/src/components/text/EmojiLabel.tsx b/valdi_modules/widgets/src/components/text/EmojiLabel.tsx
index 90e5c1a..c5b85df 100644
--- a/valdi_modules/widgets/src/components/text/EmojiLabel.tsx
+++ b/valdi_modules/widgets/src/components/text/EmojiLabel.tsx
@@ -14,7 +14,9 @@ export class EmojiLabel extends Component {
onRender(): void {
;
}
diff --git a/valdi_modules/widgets/src/components/toggle/SelectorToggle.tsx b/valdi_modules/widgets/src/components/toggle/SelectorToggle.tsx
index ecd6323..87d39a4 100644
--- a/valdi_modules/widgets/src/components/toggle/SelectorToggle.tsx
+++ b/valdi_modules/widgets/src/components/toggle/SelectorToggle.tsx
@@ -3,7 +3,7 @@ import { setTimeoutInterruptible } from 'valdi_core/src/SetTimeout';
import { Style } from 'valdi_core/src/Style';
import { View } from 'valdi_tsx/src/NativeTemplateElements';
import { SemanticColor } from 'widgets/src/styles/semanticColors';
-import res from '../../../res';
+import res from 'widgets/res';
const OUTER_INACTIVE_COLOR = SemanticColor.Icon.SECONDARY;
const INNER_INACTIVE_COLOR = SemanticColor.Background.SURFACE;
diff --git a/valdi_modules/widgets/src/components/util/ComponentDefRenderer.tsx b/valdi_modules/widgets/src/components/util/ComponentDefRenderer.tsx
index 17d395c..b2d12d7 100644
--- a/valdi_modules/widgets/src/components/util/ComponentDefRenderer.tsx
+++ b/valdi_modules/widgets/src/components/util/ComponentDefRenderer.tsx
@@ -1,4 +1,4 @@
-import { ComponentDef } from '../ComponentDef';
+import { ComponentDef } from 'widgets/src/components/ComponentDef';
interface ComponentDefRendererViewModel {
componentDef: Def;
diff --git a/valdi_modules/widgets/src/components/util/ComponentMeasurer.tsx b/valdi_modules/widgets/src/components/util/ComponentMeasurer.tsx
index 741d33c..1855fd8 100644
--- a/valdi_modules/widgets/src/components/util/ComponentMeasurer.tsx
+++ b/valdi_modules/widgets/src/components/util/ComponentMeasurer.tsx
@@ -1,5 +1,5 @@
import { Component } from 'valdi_core/src/Component';
-import { RenderFunction } from '../RenderFunction';
+import { RenderFunction } from 'widgets/src/components/RenderFunction';
import { MeasuredComponentSize } from './MeasureCache';
interface ViewModel {
diff --git a/valdi_modules/widgets/src/components/util/DragToDismissController.ts b/valdi_modules/widgets/src/components/util/DragToDismissController.ts
index 4016798..f7bf5f9 100644
--- a/valdi_modules/widgets/src/components/util/DragToDismissController.ts
+++ b/valdi_modules/widgets/src/components/util/DragToDismissController.ts
@@ -1,5 +1,5 @@
import { ScrollEvent, ScrollDragEndEvent } from 'valdi_tsx/src/GestureEvents';
-import { ScrollViewListener } from '../scroll/ScrollViewListener';
+import { ScrollViewListener } from 'widgets/src/components/scroll/ScrollViewListener';
export class DragToDismissController implements ScrollViewListener {
// The current drag session is eligible to trigger a dismiss on end
diff --git a/valdi_modules/widgets/src/components/util/MeasureCachePopulator.tsx b/valdi_modules/widgets/src/components/util/MeasureCachePopulator.tsx
index 23504a1..1624aed 100644
--- a/valdi_modules/widgets/src/components/util/MeasureCachePopulator.tsx
+++ b/valdi_modules/widgets/src/components/util/MeasureCachePopulator.tsx
@@ -1,7 +1,7 @@
import { $slot } from 'valdi_core/src/CompilerIntrinsics';
import { StatefulComponent } from 'valdi_core/src/Component';
import { Subscription } from 'valdi_rxjs/src/Subscription';
-import { RenderFunction } from '../RenderFunction';
+import { RenderFunction } from 'widgets/src/components/RenderFunction';
import { ComponentMeasurer } from './ComponentMeasurer';
import { MeasureCache, MeasuredComponentSize } from './MeasureCache';
diff --git a/valdi_modules/widgets/web/src/WidgetsWeb.ts b/valdi_modules/widgets/web/src/WidgetsWeb.ts
new file mode 100644
index 0000000..9e6b5ef
--- /dev/null
+++ b/valdi_modules/widgets/web/src/WidgetsWeb.ts
@@ -0,0 +1,228 @@
+/**
+ * Web polyglot implementations for Widgets custom views.
+ *
+ * Exports webPolyglotViews — the build system's generate_register_native_modules picks this up
+ * and registers each factory with the web view class registry at bundle load time.
+ *
+ * Class names must match the webClass attributes in the corresponding TSX components:
+ * DatePicker → SCWidgetsDatePickerWeb
+ * TimePicker → SCWidgetsTimePickerWeb
+ * IndexPicker → SCWidgetsIndexPickerWeb
+ * EmojiLabel → SCWidgetsLabelWeb
+ *
+ * Each factory returns an object with a changeAttribute(name, value) method so the
+ * web renderer can forward attribute updates from the component tree.
+ */
+
+interface AttributeHandler {
+ changeAttribute(name: string, value: unknown): void;
+}
+
+type ViewFactory = (container: HTMLElement) => AttributeHandler;
+
+// ─── DatePicker ──────────────────────────────────────────────────────────────
+
+function createDatePickerFactory(): ViewFactory {
+ return (container: HTMLElement): AttributeHandler => {
+ container.style.display = 'flex';
+ container.style.alignItems = 'center';
+ container.style.justifyContent = 'center';
+ container.style.pointerEvents = 'auto';
+
+ const input = document.createElement('input');
+ input.type = 'date';
+ input.style.fontSize = '16px';
+ input.style.padding = '8px';
+ input.style.border = '1px solid #ccc';
+ input.style.borderRadius = '6px';
+ input.style.cursor = 'pointer';
+ input.style.pointerEvents = 'auto';
+ container.appendChild(input);
+
+ let onChange: ((result: { dateSeconds: number }) => void) | null = null;
+
+ input.addEventListener('change', () => {
+ if (onChange) {
+ const dateSeconds = new Date(input.value).getTime() / 1000;
+ onChange({ dateSeconds });
+ }
+ });
+
+ return {
+ changeAttribute(name: string, value: unknown): void {
+ if (name === 'dateSeconds' && typeof value === 'number') {
+ const d = new Date(value * 1000);
+ input.value = d.toISOString().split('T')[0];
+ } else if (name === 'minimumDateSeconds' && typeof value === 'number') {
+ const d = new Date(value * 1000);
+ input.min = d.toISOString().split('T')[0];
+ } else if (name === 'maximumDateSeconds' && typeof value === 'number') {
+ const d = new Date(value * 1000);
+ input.max = d.toISOString().split('T')[0];
+ } else if (name === 'onChange') {
+ onChange = typeof value === 'function' ? (value as (result: { dateSeconds: number }) => void) : null;
+ }
+ },
+ };
+ };
+}
+
+// ─── TimePicker ──────────────────────────────────────────────────────────────
+
+function createTimePickerFactory(): ViewFactory {
+ return (container: HTMLElement): AttributeHandler => {
+ container.style.display = 'flex';
+ container.style.alignItems = 'center';
+ container.style.justifyContent = 'center';
+ container.style.pointerEvents = 'auto';
+
+ const input = document.createElement('input');
+ input.type = 'time';
+ input.style.fontSize = '16px';
+ input.style.padding = '8px';
+ input.style.border = '1px solid #ccc';
+ input.style.borderRadius = '6px';
+ input.style.cursor = 'pointer';
+ input.style.pointerEvents = 'auto';
+ container.appendChild(input);
+
+ let onChange: ((result: { hourOfDay: number; minuteOfHour: number }) => void) | null = null;
+
+ input.addEventListener('change', () => {
+ if (onChange) {
+ const parts = input.value.split(':');
+ onChange({ hourOfDay: parseInt(parts[0], 10), minuteOfHour: parseInt(parts[1], 10) });
+ }
+ });
+
+ return {
+ changeAttribute(name: string, value: unknown): void {
+ if (name === 'hourOfDay' && typeof value === 'number') {
+ const parts = (input.value || '00:00').split(':');
+ parts[0] = String(value).padStart(2, '0');
+ input.value = parts.join(':');
+ } else if (name === 'minuteOfHour' && typeof value === 'number') {
+ const parts = (input.value || '00:00').split(':');
+ parts[1] = String(value).padStart(2, '0');
+ input.value = parts.join(':');
+ } else if (name === 'onChange') {
+ onChange = typeof value === 'function' ? (value as (result: { hourOfDay: number; minuteOfHour: number }) => void) : null;
+ }
+ },
+ };
+ };
+}
+
+// ─── IndexPicker ─────────────────────────────────────────────────────────────
+
+function createIndexPickerFactory(): ViewFactory {
+ return (container: HTMLElement): AttributeHandler => {
+ container.style.display = 'flex';
+ container.style.alignItems = 'center';
+ container.style.justifyContent = 'center';
+ container.style.pointerEvents = 'auto';
+
+ const select = document.createElement('select');
+ select.style.fontSize = '16px';
+ select.style.padding = '8px';
+ select.style.border = '1px solid #ccc';
+ select.style.borderRadius = '6px';
+ select.style.cursor = 'pointer';
+ select.style.minWidth = '120px';
+ select.style.pointerEvents = 'auto';
+ container.appendChild(select);
+
+ let onChange: ((index: number) => void) | null = null;
+ let currentLabels: string[] = [];
+ let currentIndex = 0;
+
+ select.addEventListener('change', () => {
+ if (onChange) {
+ onChange(select.selectedIndex);
+ }
+ });
+
+ function rebuildOptions(): void {
+ select.innerHTML = '';
+ for (let i = 0; i < currentLabels.length; i++) {
+ const opt = document.createElement('option');
+ opt.textContent = currentLabels[i];
+ opt.value = String(i);
+ select.appendChild(opt);
+ }
+ if (currentLabels.length > 0) {
+ const clamped = Math.max(0, Math.min(currentIndex, currentLabels.length - 1));
+ select.selectedIndex = clamped;
+ }
+ }
+
+ return {
+ changeAttribute(name: string, value: unknown): void {
+ if (name === 'content' && Array.isArray(value)) {
+ const index = value[0] as number;
+ const labels = value[1] as string[];
+ if (typeof index === 'number') currentIndex = index;
+ if (Array.isArray(labels)) currentLabels = labels;
+ rebuildOptions();
+ } else if (name === 'index' && typeof value === 'number') {
+ currentIndex = value;
+ rebuildOptions();
+ } else if (name === 'labels' && Array.isArray(value)) {
+ currentLabels = value as string[];
+ rebuildOptions();
+ } else if (name === 'onChange') {
+ onChange = typeof value === 'function' ? (value as (index: number) => void) : null;
+ }
+ },
+ };
+ };
+}
+
+// ─── EmojiLabel ──────────────────────────────────────────────────────────────
+
+function createLabelFactory(): ViewFactory {
+ return (container: HTMLElement): AttributeHandler => {
+ container.style.display = 'flex';
+ container.style.alignItems = 'center';
+
+ const span = document.createElement('span');
+ span.style.fontSize = 'inherit';
+ span.style.lineHeight = 'inherit';
+ container.appendChild(span);
+
+ return {
+ changeAttribute(name: string, value: unknown): void {
+ if (name === 'value' && (typeof value === 'string' || value == null)) {
+ span.textContent = (value as string) || '';
+ } else if (name === 'color' && typeof value === 'number') {
+ const r = (value >> 24) & 0xff;
+ const g = (value >> 16) & 0xff;
+ const b = (value >> 8) & 0xff;
+ const a = (value & 0xff) / 255;
+ span.style.color = `rgba(${r},${g},${b},${a})`;
+ } else if (name === 'numberOfLines' && typeof value === 'number') {
+ if (value > 0) {
+ span.style.display = '-webkit-box';
+ (span.style as any).webkitLineClamp = String(value);
+ (span.style as any).webkitBoxOrient = 'vertical';
+ span.style.overflow = 'hidden';
+ } else {
+ span.style.display = '';
+ (span.style as any).webkitLineClamp = '';
+ (span.style as any).webkitBoxOrient = '';
+ span.style.overflow = '';
+ }
+ }
+ },
+ };
+ };
+}
+
+// ─── Registry export ─────────────────────────────────────────────────────────
+
+export const webPolyglotViews: Record = {
+ SCWidgetsDatePickerWeb: createDatePickerFactory(),
+ SCWidgetsTimePickerWeb: createTimePickerFactory(),
+ SCWidgetsIndexPickerWeb: createIndexPickerFactory(),
+ SCWidgetsLabelWeb: createLabelFactory(),
+};
diff --git a/valdi_modules/widgets/web/tsconfig.json b/valdi_modules/widgets/web/tsconfig.json
new file mode 100644
index 0000000..1bb1ef3
--- /dev/null
+++ b/valdi_modules/widgets/web/tsconfig.json
@@ -0,0 +1,9 @@
+{
+ "compilerOptions": {
+ "target": "ES2016",
+ "module": "commonjs",
+ "strict": true,
+ "lib": ["dom", "ES2019"]
+ },
+ "include": ["src/**/*.ts"]
+}