diff --git a/.github/workflows/deploy-demo.yml b/.github/workflows/deploy-demo.yml new file mode 100644 index 0000000..28c4997 --- /dev/null +++ b/.github/workflows/deploy-demo.yml @@ -0,0 +1,44 @@ +name: Deploy Demo to GitHub Pages + +on: + push: + branches: [main] + workflow_dispatch: + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: pages + cancel-in-progress: true + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 20 + cache: npm + + - run: npm ci + + - run: npx vite build --config demo/vite.config.ts + + - uses: actions/upload-pages-artifact@v3 + with: + path: demo-dist + + deploy: + needs: build + runs-on: ubuntu-latest + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - id: deployment + uses: actions/deploy-pages@v4 diff --git a/README.md b/README.md index 0707bdc..80c164d 100644 --- a/README.md +++ b/README.md @@ -13,31 +13,33 @@ and name are likely to change drastically before a major version release. Each component are meant to be copied from this repo to your own project and customized to your liking. There are no CLI tools to help. just copy and paste from github. -| `form_props` helper | Component | [Vanilla] | [Mantine] | ? | -| :-------------------------------------- | :--------------------- | :----------------- | :------------------- | :------------------- | -| `f.text_field` | Checkbox | :heavy_check_mark: | :heavy_check_mark: | :white_large_square: | -| `f.collection_check_boxes` | CollectionCheckboxes | :heavy_check_mark: | :heavy_check_mark: | :white_large_square: | -| `f.collection_radio_buttons` | CollectionRadioButtons | :heavy_check_mark: | :heavy_check_mark: | :white_large_square: | -| `f.color_field` | ColorField | :heavy_check_mark: | :heavy_check_mark: | :white_large_square: | -| `f.date_field` | DateField | :heavy_check_mark: | :heavy_check_mark: | :white_large_square: | -| `f.datetime_local_field` | DateTimeLocalField | :heavy_check_mark: | :heavy_check_mark: | :white_large_square: | -| `f.email_field` | EmailField | :heavy_check_mark: | :heavy_check_mark: | :white_large_square: | -| `f.month_field` | MonthField | :heavy_check_mark: | :heavy_check_mark: | :white_large_square: | -| `f.number_field` | NumberField | :heavy_check_mark: | :heavy_check_mark: | :white_large_square: | -| `f.password_field` | PasswordField | :heavy_check_mark: | :heavy_check_mark: | :white_large_square: | -| `f.range_field` | RangeField | :heavy_check_mark: | :white_large_square: | :white_large_square: | -| `f.search_field` | SearchField | :heavy_check_mark: | :heavy_check_mark: | :white_large_square: | -| `f.select` (`multiple: true` supported) | Select | :heavy_check_mark: | :heavy_check_mark: | :white_large_square: | -| `f.tel_field` | TelField | :heavy_check_mark: | :heavy_check_mark: | :white_large_square: | -| `f.file_field` | FileField | :heavy_check_mark: | :heavy_check_mark: | :white_large_square: | -| `f.text_field` | TextField | :heavy_check_mark: | :heavy_check_mark: | :white_large_square: | -| `f.time_field` | TimeField | :heavy_check_mark: | :heavy_check_mark: | :white_large_square: | -| `f.url_field` | UrlField | :heavy_check_mark: | :heavy_check_mark: | :white_large_square: | -| `f.text_area` | TextArea | :heavy_check_mark: | :heavy_check_mark: | :white_large_square: | -| `f.grouped_collection_select` | Select | :heavy_check_mark: | :heavy_check_mark: | :white_large_square: | -| `f.weekday_select` | Select | :heavy_check_mark: | :heavy_check_mark: | :white_large_square: | -| `f.time_zone_select` | Select | :heavy_check_mark: | :heavy_check_mark: | :white_large_square: | -| `f.submit` | SubmitButton | :heavy_check_mark: | :heavy_check_mark: | :white_large_square: | +> Legend: :heavy_check_mark: library component | :globe_with_meridians: native HTML input | :x: not supported + +| `form_props` helper | Component | [Vanilla] | [Ark UI] | [Chakra UI] | [React Spectrum] | [Mantine] | [HeroUI] | [MUI] | [React Aria] | +| :-------------------------------------- | :--------------------- | :----------------- | :------------------- | :----------------- | :----------------- | :------------------- | :----------------- | :----------------- | :----------------- | +| `f.text_field` | TextField | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | +| `f.email_field` | EmailField | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | +| `f.password_field` | PasswordField | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | +| `f.number_field` | NumberField | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | +| `f.date_field` | DateField | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | +| `f.datetime_local_field` | DateTimeLocalField | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | +| `f.time_field` | TimeField | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | +| `f.search_field` | SearchField | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | +| `f.tel_field` | TelField | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | +| `f.url_field` | UrlField | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | +| `f.color_field` | ColorField | :globe_with_meridians: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :globe_with_meridians: | :heavy_check_mark: | +| `f.month_field` | MonthField | :globe_with_meridians: | :globe_with_meridians: | :globe_with_meridians: | :globe_with_meridians: | :heavy_check_mark: | :globe_with_meridians: | :globe_with_meridians: | :globe_with_meridians: | +| `f.range_field` | RangeField | :globe_with_meridians: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | +| `f.file_field` | FileField | :globe_with_meridians: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :globe_with_meridians: | :globe_with_meridians: | :heavy_check_mark: | +| `f.check_box` | Checkbox | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | +| `f.collection_check_boxes` | CollectionCheckboxes | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | +| `f.collection_radio_buttons` | CollectionRadioButtons | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | +| `f.select` (`multiple: true` supported) | Select | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | +| `f.text_area` | TextArea | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | +| `f.grouped_collection_select` | Select | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | +| `f.weekday_select` | Select | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | +| `f.time_zone_select` | Select | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | +| `f.submit` | SubmitButton | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | ## Installation @@ -138,14 +140,62 @@ const {form, extras, inputs} = newPostForm Vanilla wrappers wrap around basic React HTML tags. If you want to build wrappers of your own, you can start here and use other UI wrappers as reference. +## Ark UI + +To use the Ark UI wrappers, add the following library before copying: + +``` +yarn add @ark-ui/react +``` + +## Chakra UI + +To use the Chakra UI wrappers, add the following library before copying: + +``` +yarn add @chakra-ui/react +``` + +## React Spectrum + +To use the React Spectrum S2 wrappers, add the following libraries before copying: + +``` +yarn add @react-spectrum/s2 @internationalized/date +``` + ## Mantine -To use the Mantine wrappers, add the following libraries to your libraries before copying +To use the Mantine wrappers, add the following libraries before copying: + +``` +yarn add @mantine/core @mantine/dates +``` + +## HeroUI + +To use the HeroUI wrappers, add the following libraries before copying: + +``` +yarn add @heroui/react @internationalized/date +``` + +Requires Tailwind CSS v4 with `@heroui/styles`. + +## MUI + +To use the MUI wrappers, add the following library before copying: + +``` +yarn add @mui/material @emotion/react @emotion/styled +``` + +## React Aria + +To use the React Aria wrappers, add the following libraries before copying: ``` -yarn add dayjs -yarn add @mantine/core -yarn add @mantine/dates +yarn add react-aria-components @internationalized/date ``` ## Contributors @@ -154,3 +204,11 @@ Thank you, [contributors]! [contributors]: https://github.com/thoughtbot/candy_wrapper/graphs/contributors [form_props]: https://github.com/thoughtbot/form_props +[Vanilla]: wrappers/ts/vanilla +[Ark UI]: wrappers/ts/ark/v5 +[Chakra UI]: wrappers/ts/chakra/v3 +[React Spectrum]: wrappers/ts/react-spectrum/s2 +[Mantine]: wrappers/ts/mantine/v9 +[HeroUI]: wrappers/ts/heroui/v3 +[MUI]: wrappers/ts/mui/v9 +[React Aria]: wrappers/ts/react-aria/v1 diff --git a/demo/frames/ark.css b/demo/frames/ark.css new file mode 100644 index 0000000..7d0c641 --- /dev/null +++ b/demo/frames/ark.css @@ -0,0 +1,440 @@ +/* Minimal styles for Ark UI headless components */ + +:root { + --ark-accent: #EB5E41; + --ark-accent-fg: #fff; + --ark-bg: #fff; + --ark-fg: #1a1a1a; + --ark-fg-muted: #71717a; + --ark-border: #d4d4d8; + --ark-border-focus: #EB5E41; + --ark-error: #dc2626; + --ark-bg-muted: #f4f4f5; + --ark-radius: 6px; + --ark-font: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; +} + +body { + font-family: var(--ark-font); + color: var(--ark-fg); +} + + +/* Field */ +[data-scope="field"][data-part="root"] { + display: flex; + flex-direction: column; + gap: 6px; +} + +[data-scope="field"][data-part="label"] { + font-size: 14px; + font-weight: 500; +} + +[data-scope="field"][data-part="input"], +[data-scope="field"][data-part="textarea"] { + border: 1px solid var(--ark-border); + border-radius: var(--ark-radius); + padding: 8px 12px; + font-size: 14px; + outline: none; + width: 100%; + box-sizing: border-box; +} + +[data-scope="field"][data-part="input"]:focus, +[data-scope="field"][data-part="textarea"]:focus { + border-color: var(--ark-border-focus); + box-shadow: 0 0 0 1px var(--ark-border-focus); +} + +[data-scope="field"][data-part="error-text"] { + color: var(--ark-error); + font-size: 13px; +} + +/* Checkbox */ +[data-scope="checkbox"][data-part="root"] { + display: flex; + align-items: center; + gap: 8px; + cursor: pointer; +} + +[data-scope="checkbox"][data-part="label"] { + font-size: 14px; + font-weight: 500; + user-select: none; +} + +[data-scope="checkbox"][data-part="control"] { + width: 18px; + height: 18px; + border: 1px solid var(--ark-border); + border-radius: 4px; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + font-size: 12px; + color: var(--ark-accent-fg); + transition: background 0.15s, border-color 0.15s; +} + +[data-scope="checkbox"][data-part="control"][data-state="checked"] { + background: var(--ark-accent); + border-color: var(--ark-accent); +} + +/* Radio Group */ +[data-scope="radio-group"][data-part="root"] { + display: flex; + flex-direction: column; + gap: 8px; +} + +[data-scope="radio-group"][data-part="label"] { + font-size: 14px; + font-weight: 500; +} + +[data-scope="radio-group"][data-part="item"] { + display: flex; + align-items: center; + gap: 8px; + cursor: pointer; +} + +[data-scope="radio-group"][data-part="item-control"] { + width: 18px; + height: 18px; + border: 1px solid var(--ark-border); + border-radius: 50%; + flex-shrink: 0; + transition: background 0.15s, border-color 0.15s; +} + +[data-scope="radio-group"][data-part="item-control"][data-state="checked"] { + background: var(--ark-accent); + border-color: var(--ark-accent); + outline: 3px solid var(--ark-bg); + outline-offset: -4px; +} + +[data-scope="radio-group"][data-part="item-text"] { + font-size: 14px; + font-weight: 500; +} + +/* Select */ +[data-scope="select"][data-part="root"] { + display: flex; + flex-direction: column; + gap: 6px; + width: 100%; +} + +[data-scope="select"][data-part="label"] { + font-size: 14px; + font-weight: 500; +} + +[data-scope="select"][data-part="trigger"] { + border: 1px solid var(--ark-border); + border-radius: var(--ark-radius); + padding: 8px 12px; + font-size: 14px; + cursor: pointer; + display: flex; + justify-content: space-between; + align-items: flex-start; + text-align: left; + background: var(--ark-bg); + width: 100%; +} + +[data-scope="select"][data-part="trigger"]:focus { + border-color: var(--ark-border-focus); + box-shadow: 0 0 0 1px var(--ark-border-focus); + outline: none; +} + +[data-scope="select"][data-part="content"] { + background: var(--ark-bg); + border: 1px solid var(--ark-border); + border-radius: var(--ark-radius); + box-shadow: 0 4px 12px rgba(0,0,0,0.1); + padding: 4px; + z-index: 1000; + display: flex; + flex-direction: column; +} + +[data-scope="select"][data-part="content"][hidden] { + display: none; +} + +[data-scope="select"][data-part="item"] { + padding: 6px 8px; + border-radius: 4px; + cursor: pointer; + font-size: 14px; + display: flex; + justify-content: space-between; + align-items: center; + text-align: left; +} + +[data-scope="select"][data-part="item"][data-highlighted] { + background: var(--ark-bg-muted); +} + +[data-scope="select"][data-part="item"][data-state="checked"] { + color: var(--ark-accent); +} + +[data-scope="select"][data-part="item-text"] { + flex: 1; + text-align: left; +} + +[data-scope="select"][data-part="item-indicator"] { + color: var(--ark-accent); + flex-shrink: 0; +} + +/* Number Input */ +[data-scope="number-input"][data-part="root"] { + display: flex; + flex-direction: column; + gap: 6px; +} + +[data-scope="number-input"][data-part="label"] { + font-size: 14px; + font-weight: 500; +} + +[data-scope="number-input"][data-part="control"] { + display: flex; + border: 1px solid var(--ark-border); + border-radius: var(--ark-radius); + overflow: hidden; +} + +[data-scope="number-input"][data-part="input"] { + border: none; + padding: 8px 12px; + font-size: 14px; + outline: none; + flex: 1; + min-width: 0; +} + +[data-scope="number-input"][data-part="increment-trigger"], +[data-scope="number-input"][data-part="decrement-trigger"] { + border: none; + border-left: 1px solid var(--ark-border); + background: var(--ark-bg-muted); + padding: 4px 10px; + cursor: pointer; + font-size: 14px; + color: var(--ark-fg-muted); +} + +[data-scope="number-input"][data-part="increment-trigger"]:hover, +[data-scope="number-input"][data-part="decrement-trigger"]:hover { + background: var(--ark-border); +} + +/* Slider */ +[data-scope="slider"][data-part="root"] { + display: flex; + flex-direction: column; + gap: 6px; + width: 100%; +} + +[data-scope="slider"][data-part="label"] { + font-size: 14px; + font-weight: 500; +} + +[data-scope="slider"][data-part="value-text"] { + font-size: 13px; + color: var(--ark-fg-muted); +} + +[data-scope="slider"][data-part="control"] { + position: relative; + display: flex; + align-items: center; + height: 20px; +} + +[data-scope="slider"][data-part="track"] { + background: var(--ark-bg-muted); + border-radius: 999px; + height: 6px; + flex: 1; + overflow: hidden; +} + +[data-scope="slider"][data-part="range"] { + background: var(--ark-accent); + height: 100%; +} + +[data-scope="slider"][data-part="thumb"] { + width: 20px; + height: 20px; + border-radius: 50%; + background: var(--ark-bg); + border: 2px solid var(--ark-accent); + box-shadow: 0 1px 3px rgba(0,0,0,0.15); + outline: none; + cursor: grab; +} + +[data-scope="slider"][data-part="thumb"]:active { + cursor: grabbing; +} + +/* Color Picker */ +[data-scope="color-picker"][data-part="root"] { + display: flex; + flex-direction: column; + gap: 6px; +} + +[data-scope="color-picker"][data-part="label"] { + font-size: 14px; + font-weight: 500; +} + +[data-scope="color-picker"][data-part="control"] { + display: flex; + gap: 8px; + align-items: center; +} + +[data-scope="color-picker"][data-part="channel-input"] { + border: 1px solid var(--ark-border); + border-radius: var(--ark-radius); + padding: 8px 12px; + font-size: 14px; + outline: none; + width: 120px; +} + +[data-scope="color-picker"][data-part="channel-input"]:focus { + border-color: var(--ark-border-focus); + box-shadow: 0 0 0 1px var(--ark-border-focus); +} + +[data-scope="color-picker"][data-part="trigger"] { + border: 1px solid var(--ark-border); + border-radius: var(--ark-radius); + padding: 2px; + cursor: pointer; + background: none; +} + +[data-scope="color-picker"][data-part="swatch"] { + width: 28px; + height: 28px; + border-radius: 4px; +} + +[data-scope="color-picker"][data-part="content"] { + background: var(--ark-bg); + border: 1px solid var(--ark-border); + border-radius: 8px; + box-shadow: 0 4px 16px rgba(0,0,0,0.12); + padding: 12px; + z-index: 1000; + display: flex; + flex-direction: column; + gap: 8px; +} + +[data-scope="color-picker"][data-part="content"][hidden] { + display: none; +} + +[data-scope="color-picker"][data-part="area"] { + border-radius: var(--ark-radius); + height: 140px; + overflow: hidden; +} + +[data-scope="color-picker"][data-part="area-background"] { + height: 100%; +} + +[data-scope="color-picker"][data-part="area-thumb"] { + width: 16px; + height: 16px; + border-radius: 50%; + box-shadow: 0 0 0 2px white, 0 0 2px 1px black; +} + +[data-scope="color-picker"][data-part="channel-slider"] { + border-radius: var(--ark-radius); +} + +[data-scope="color-picker"][data-part="channel-slider-track"] { + height: 10px; + border-radius: var(--ark-radius); +} + +[data-scope="color-picker"][data-part="channel-slider-thumb"] { + width: 14px; + height: 14px; + border-radius: 50%; + box-shadow: 0 0 0 2px white, 0 0 2px 1px black; + transform: translate(-50%, -50%); +} + +/* File Upload */ +[data-scope="file-upload"][data-part="root"] { + display: flex; + flex-direction: column; + gap: 6px; +} + +[data-scope="file-upload"][data-part="label"] { + font-size: 14px; + font-weight: 500; +} + +[data-scope="file-upload"][data-part="trigger"] { + border: 1px solid var(--ark-border); + border-radius: var(--ark-radius); + padding: 8px 16px; + cursor: pointer; + background: var(--ark-bg); + font-size: 14px; + display: inline-block; +} + +[data-scope="file-upload"][data-part="trigger"]:hover { + background: var(--ark-bg-muted); +} + +/* Submit button */ +button[type="submit"] { + background: var(--ark-accent); + color: var(--ark-accent-fg); + border: none; + border-radius: var(--ark-radius); + padding: 8px 20px; + font-size: 14px; + font-weight: 600; + cursor: pointer; +} + +button[type="submit"]:hover { + opacity: 0.9; +} diff --git a/demo/frames/ark.html b/demo/frames/ark.html new file mode 100644 index 0000000..434cdb5 --- /dev/null +++ b/demo/frames/ark.html @@ -0,0 +1,12 @@ + + + + + + Ark UI + + +
+ + + diff --git a/demo/frames/ark.tsx b/demo/frames/ark.tsx new file mode 100644 index 0000000..e06cc0a --- /dev/null +++ b/demo/frames/ark.tsx @@ -0,0 +1,53 @@ +import './ark.css' +import { + TextField, + EmailField, + PasswordField, + NumberField, + DateField, + DateTimeLocalField, + TimeField, + MonthField, + ColorField, + SearchField, + TelField, + UrlField, + RangeField, + Checkbox, + CollectionCheckboxes, + CollectionRadioButtons, + Select, + TextArea, + FileField, + SubmitButton, + ValidationContext, +} from '../../wrappers/ts/ark/v5/index' +import { renderFrame } from './renderFrame' + +renderFrame( + { + TextField, + EmailField, + PasswordField, + NumberField, + DateField, + DateTimeLocalField, + TimeField, + MonthField, + ColorField, + SearchField, + TelField, + UrlField, + RangeField, + Checkbox, + CollectionCheckboxes, + CollectionRadioButtons, + Select, + MultiSelect: Select, + TextArea, + FileField, + SubmitButton, + }, + undefined, + ValidationContext +) diff --git a/demo/frames/chakra.html b/demo/frames/chakra.html new file mode 100644 index 0000000..8317251 --- /dev/null +++ b/demo/frames/chakra.html @@ -0,0 +1,12 @@ + + + + + + Chakra UI + + +
+ + + diff --git a/demo/frames/chakra.tsx b/demo/frames/chakra.tsx new file mode 100644 index 0000000..6cde671 --- /dev/null +++ b/demo/frames/chakra.tsx @@ -0,0 +1,58 @@ +import React from 'react' +import { ChakraProvider, defaultSystem } from '@chakra-ui/react' +import { + TextField, + EmailField, + PasswordField, + NumberField, + DateField, + DateTimeLocalField, + TimeField, + MonthField, + ColorField, + SearchField, + TelField, + UrlField, + RangeField, + Checkbox, + CollectionCheckboxes, + CollectionRadioButtons, + Select, + TextArea, + FileField, + SubmitButton, + ValidationContext, +} from '../../wrappers/ts/chakra/v3/index' +import { renderFrame } from './renderFrame' + +const Provider = ({ children }: { children: React.ReactNode }) => ( + {children} +) + +renderFrame( + { + TextField, + EmailField, + PasswordField, + NumberField, + DateField, + DateTimeLocalField, + TimeField, + MonthField, + ColorField, + SearchField, + TelField, + UrlField, + RangeField, + Checkbox, + CollectionCheckboxes, + CollectionRadioButtons, + Select, + MultiSelect: Select, + TextArea, + FileField, + SubmitButton, + }, + Provider, + ValidationContext +) diff --git a/demo/frames/heroui.css b/demo/frames/heroui.css new file mode 100644 index 0000000..ed35a33 --- /dev/null +++ b/demo/frames/heroui.css @@ -0,0 +1,2 @@ +@import "tailwindcss"; +@import "@heroui/styles"; diff --git a/demo/frames/heroui.html b/demo/frames/heroui.html new file mode 100644 index 0000000..322e2a5 --- /dev/null +++ b/demo/frames/heroui.html @@ -0,0 +1,12 @@ + + + + + + HeroUI + + +
+ + + diff --git a/demo/frames/heroui.tsx b/demo/frames/heroui.tsx new file mode 100644 index 0000000..5e50507 --- /dev/null +++ b/demo/frames/heroui.tsx @@ -0,0 +1,53 @@ +import './heroui.css' +import { + TextField, + EmailField, + PasswordField, + NumberField, + DateField, + DateTimeLocalField, + TimeField, + MonthField, + ColorField, + SearchField, + TelField, + UrlField, + RangeField, + Checkbox, + CollectionCheckboxes, + CollectionRadioButtons, + Select, + TextArea, + FileField, + SubmitButton, + ValidationContext, +} from '../../wrappers/ts/heroui/v3/index' +import { renderFrame } from './renderFrame' + +renderFrame( + { + TextField, + EmailField, + PasswordField, + NumberField, + DateField, + DateTimeLocalField, + TimeField, + MonthField, + ColorField, + SearchField, + TelField, + UrlField, + RangeField, + Checkbox, + CollectionCheckboxes, + CollectionRadioButtons, + Select, + MultiSelect: Select, + TextArea, + FileField, + SubmitButton, + }, + undefined, + ValidationContext +) diff --git a/demo/frames/mantine.html b/demo/frames/mantine.html new file mode 100644 index 0000000..bb955b9 --- /dev/null +++ b/demo/frames/mantine.html @@ -0,0 +1,12 @@ + + + + + + Mantine + + +
+ + + diff --git a/demo/frames/mantine.tsx b/demo/frames/mantine.tsx new file mode 100644 index 0000000..8a939d6 --- /dev/null +++ b/demo/frames/mantine.tsx @@ -0,0 +1,60 @@ +import React from 'react' +import { MantineProvider } from '@mantine/core' +import '@mantine/core/styles.css' +import '@mantine/dates/styles.css' +import { + TextField, + EmailField, + PasswordField, + NumberField, + DateField, + DateTimeLocalField, + TimeField, + MonthField, + ColorField, + SearchField, + TelField, + UrlField, + RangeField, + Checkbox, + CollectionCheckboxes, + CollectionRadioButtons, + Select, + TextArea, + FileField, + SubmitButton, + ValidationContext, +} from '../../wrappers/ts/mantine/v9/index' +import { renderFrame } from './renderFrame' + +const Provider = ({ children }: { children: React.ReactNode }) => ( + {children} +) + +renderFrame( + { + TextField, + EmailField, + PasswordField, + NumberField, + DateField, + DateTimeLocalField, + TimeField, + MonthField, + ColorField, + SearchField, + TelField, + UrlField, + RangeField, + Checkbox, + CollectionCheckboxes, + CollectionRadioButtons, + Select, + MultiSelect: Select, + TextArea, + FileField, + SubmitButton, + }, + Provider, + ValidationContext +) diff --git a/demo/frames/mui.html b/demo/frames/mui.html new file mode 100644 index 0000000..36af31e --- /dev/null +++ b/demo/frames/mui.html @@ -0,0 +1,12 @@ + + + + + + MUI + + +
+ + + diff --git a/demo/frames/mui.tsx b/demo/frames/mui.tsx new file mode 100644 index 0000000..00c9cba --- /dev/null +++ b/demo/frames/mui.tsx @@ -0,0 +1,52 @@ +import { + TextFieldComponent as TextField, + EmailField, + PasswordField, + NumberField, + DateField, + DateTimeLocalField, + TimeField, + MonthField, + ColorField, + SearchField, + TelField, + UrlField, + RangeField, + Checkbox, + CollectionCheckboxes, + CollectionRadioButtons, + Select, + TextArea, + FileField, + SubmitButton, + ValidationContext, +} from '../../wrappers/ts/mui/v9/index' +import { renderFrame } from './renderFrame' + +renderFrame( + { + TextField, + EmailField, + PasswordField, + NumberField, + DateField, + DateTimeLocalField, + TimeField, + MonthField, + ColorField, + SearchField, + TelField, + UrlField, + RangeField, + Checkbox, + CollectionCheckboxes, + CollectionRadioButtons, + Select, + MultiSelect: Select, + TextArea, + FileField, + SubmitButton, + }, + undefined, + ValidationContext +) diff --git a/demo/frames/react-aria.css b/demo/frames/react-aria.css new file mode 100644 index 0000000..fa3148e --- /dev/null +++ b/demo/frames/react-aria.css @@ -0,0 +1,122 @@ +/* Minimal styles for React Aria Components */ + +.react-aria-Checkbox { + display: flex; + align-items: center; + gap: 8px; + cursor: pointer; + font-size: 14px; +} + +.react-aria-Checkbox .checkbox { + width: 18px; + height: 18px; + border: 2px solid #71717a; + border-radius: 4px; + display: flex; + align-items: center; + justify-content: center; + transition: background 0.15s, border-color 0.15s; +} + +.react-aria-Checkbox[data-selected] .checkbox { + background: #1a1a1a; + border-color: #1a1a1a; +} + +.react-aria-Checkbox .checkbox svg { + width: 14px; + height: 14px; + fill: none; + stroke: white; + stroke-width: 3; + stroke-dasharray: 22; + stroke-dashoffset: 66; +} + +.react-aria-Checkbox[data-selected] .checkbox svg { + stroke-dashoffset: 44; +} + +.react-aria-Radio { + display: flex; + align-items: center; + gap: 8px; + cursor: pointer; + font-size: 14px; +} + +.react-aria-Radio::before { + content: ''; + display: block; + width: 18px; + height: 18px; + border: 2px solid #71717a; + border-radius: 50%; + box-sizing: border-box; + transition: background 0.15s, border-color 0.15s; +} + +.react-aria-Radio[data-selected]::before { + border-color: #1a1a1a; + border-width: 6px; +} + +.react-aria-RadioGroup { + display: flex; + flex-direction: column; + gap: 6px; +} + +/* Slider */ +.react-aria-Slider { + display: flex; + flex-direction: column; + gap: 4px; + width: 100%; +} + +.react-aria-Slider [slot="label"], +.react-aria-Slider [slot="output"] { + font-size: 14px; +} + +.react-aria-SliderTrack { + position: relative; + height: 20px; + width: 100%; +} + +.react-aria-SliderTrack::before { + content: ''; + display: block; + position: absolute; + top: 50%; + transform: translateY(-50%); + width: 100%; + height: 4px; + background: #d4d4d8; + border-radius: 999px; +} + +.react-aria-SliderThumb { + width: 18px; + height: 18px; + border-radius: 50%; + background: #fff; + border: 2px solid #1a1a1a; + position: absolute; + top: 50%; + transform: translate(-50%, -50%); + cursor: grab; +} + +.react-aria-SliderThumb:active { + cursor: grabbing; +} + +.react-aria-SliderOutput { + font-size: 13px; + color: #71717a; + align-self: flex-end; +} diff --git a/demo/frames/react-aria.html b/demo/frames/react-aria.html new file mode 100644 index 0000000..21b5961 --- /dev/null +++ b/demo/frames/react-aria.html @@ -0,0 +1,12 @@ + + + + + + React Aria + + +
+ + + diff --git a/demo/frames/react-aria.tsx b/demo/frames/react-aria.tsx new file mode 100644 index 0000000..d4be128 --- /dev/null +++ b/demo/frames/react-aria.tsx @@ -0,0 +1,53 @@ +import './react-aria.css' +import { + TextField, + EmailField, + PasswordField, + NumberField, + DateField, + DateTimeLocalField, + TimeField, + MonthField, + ColorField, + SearchField, + TelField, + UrlField, + RangeField, + Checkbox, + CollectionCheckboxes, + CollectionRadioButtons, + Select, + TextArea, + FileField, + SubmitButton, + ValidationContext, +} from '../../wrappers/ts/react-aria/v1/index' +import { renderFrame } from './renderFrame' + +renderFrame( + { + TextField, + EmailField, + PasswordField, + NumberField, + DateField, + DateTimeLocalField, + TimeField, + MonthField, + ColorField, + SearchField, + TelField, + UrlField, + RangeField, + Checkbox, + CollectionCheckboxes, + CollectionRadioButtons, + Select, + MultiSelect: Select, + TextArea, + FileField, + SubmitButton, + }, + undefined, + ValidationContext +) diff --git a/demo/frames/react-spectrum.html b/demo/frames/react-spectrum.html new file mode 100644 index 0000000..f2702ea --- /dev/null +++ b/demo/frames/react-spectrum.html @@ -0,0 +1,12 @@ + + + + + + React Spectrum + + +
+ + + diff --git a/demo/frames/react-spectrum.tsx b/demo/frames/react-spectrum.tsx new file mode 100644 index 0000000..1d971da --- /dev/null +++ b/demo/frames/react-spectrum.tsx @@ -0,0 +1,52 @@ +import { + TextField, + EmailField, + PasswordField, + NumberField, + DateField, + DateTimeLocalField, + TimeField, + MonthField, + ColorField, + SearchField, + TelField, + UrlField, + RangeField, + Checkbox, + CollectionCheckboxes, + CollectionRadioButtons, + Select, + TextArea, + FileField, + SubmitButton, + ValidationContext, +} from '../../wrappers/ts/react-spectrum/s2/index' +import { renderFrame } from './renderFrame' + +renderFrame( + { + TextField, + EmailField, + PasswordField, + NumberField, + DateField, + DateTimeLocalField, + TimeField, + MonthField, + ColorField, + SearchField, + TelField, + UrlField, + RangeField, + Checkbox, + CollectionCheckboxes, + CollectionRadioButtons, + Select, + MultiSelect: Select, + TextArea, + FileField, + SubmitButton, + }, + undefined, + ValidationContext +) diff --git a/demo/frames/renderFrame.tsx b/demo/frames/renderFrame.tsx new file mode 100644 index 0000000..b0a0492 --- /dev/null +++ b/demo/frames/renderFrame.tsx @@ -0,0 +1,66 @@ +import React, { useState, useEffect } from 'react' +import { createRoot } from 'react-dom/client' +import { payloads, errorPayloads, type FieldType } from '../payloads' + +type ComponentMap = Partial>> + +export function renderFrame( + components: ComponentMap, + Provider?: React.ComponentType<{ children: React.ReactNode }>, + ValidationContext?: React.Context> +) { + function App() { + const [field, setField] = useState('TextField') + const [showErrors, setShowErrors] = useState(false) + + useEffect(() => { + function onMessage(e: MessageEvent) { + if (e.data?.type === 'SELECT_FIELD') { + setField(e.data.field as FieldType) + } + if (e.data?.type === 'TOGGLE_ERRORS') { + setShowErrors(e.data.enabled) + } + } + window.addEventListener('message', onMessage) + return () => window.removeEventListener('message', onMessage) + }, []) + + const Component = components[field] + const props = payloads[field] + const errorData = showErrors ? errorPayloads[field] : undefined + + if (!Component) { + return
Not supported
+ } + + const errorKey = errorData?.errorKey + const errors = errorData?.errors ?? {} + + const rendered = ( +
+ +
+ ) + + if (ValidationContext) { + return ( + + {rendered} + + ) + } + + return rendered + } + + const root = createRoot(document.getElementById('root')!) + const content = Provider ? ( + + + + ) : ( + + ) + root.render(content) +} diff --git a/demo/frames/vanilla.html b/demo/frames/vanilla.html new file mode 100644 index 0000000..d5ca7c5 --- /dev/null +++ b/demo/frames/vanilla.html @@ -0,0 +1,12 @@ + + + + + + Vanilla + + +
+ + + diff --git a/demo/frames/vanilla.tsx b/demo/frames/vanilla.tsx new file mode 100644 index 0000000..3eed184 --- /dev/null +++ b/demo/frames/vanilla.tsx @@ -0,0 +1,52 @@ +import { + TextField, + EmailField, + PasswordField, + NumberField, + DateField, + DateTimeLocalField, + TimeField, + MonthField, + ColorField, + SearchField, + TelField, + UrlField, + RangeField, + Checkbox, + CollectionCheckboxes, + CollectionRadioButtons, + Select, + TextArea, + FileField, + SubmitButton, + ValidationContext, +} from '../../wrappers/ts/vanilla/index' +import { renderFrame } from './renderFrame' + +renderFrame( + { + TextField, + EmailField, + PasswordField, + NumberField, + DateField, + DateTimeLocalField, + TimeField, + MonthField, + ColorField, + SearchField, + TelField, + UrlField, + RangeField, + Checkbox, + CollectionCheckboxes, + CollectionRadioButtons, + Select, + MultiSelect: Select, + TextArea, + FileField, + SubmitButton, + }, + undefined, + ValidationContext +) diff --git a/demo/index.html b/demo/index.html new file mode 100644 index 0000000..f24a401 --- /dev/null +++ b/demo/index.html @@ -0,0 +1,499 @@ + + + + + + candy_wrapper — Demo + + + + + + + + +
+
+

TextField

+ Same payload → 8 libraries +
+ + +
+
+
+ +
+
+
+
JSON
+
+
+
+
Usage
+
+
+
+
+
+ + + + diff --git a/demo/payloads.ts b/demo/payloads.ts new file mode 100644 index 0000000..a6f7d3e --- /dev/null +++ b/demo/payloads.ts @@ -0,0 +1,327 @@ +export const FIELD_TYPES = [ + 'TextField', + 'EmailField', + 'PasswordField', + 'NumberField', + 'DateField', + 'DateTimeLocalField', + 'TimeField', + 'MonthField', + 'ColorField', + 'SearchField', + 'TelField', + 'UrlField', + 'RangeField', + 'Checkbox', + 'CollectionCheckboxes', + 'CollectionRadioButtons', + 'Select', + 'MultiSelect', + 'TextArea', + 'FileField', + 'SubmitButton', +] as const + +export type FieldType = (typeof FIELD_TYPES)[number] + +export const errorPayloads: Partial }>> = { + TextField: { errorKey: 'name', errors: { name: 'Name is required' } }, + EmailField: { errorKey: 'email', errors: { email: 'Invalid email address' } }, + PasswordField: { errorKey: 'password', errors: { password: 'Password is too short' } }, + NumberField: { errorKey: 'quantity', errors: { quantity: 'Must be between 0 and 100' } }, + DateField: { errorKey: 'date', errors: { date: 'Date is required' } }, + DateTimeLocalField: { errorKey: 'starts_at', errors: { starts_at: 'Start time is required' } }, + TimeField: { errorKey: 'time', errors: { time: 'Invalid time' } }, + MonthField: { errorKey: 'month', errors: { month: 'Month is required' } }, + ColorField: { errorKey: 'primary_color', errors: { primary_color: 'Invalid color' } }, + SearchField: { errorKey: 'search', errors: { search: 'No results found' } }, + TelField: { errorKey: 'phone', errors: { phone: 'Invalid phone number' } }, + UrlField: { errorKey: 'website', errors: { website: 'Invalid URL' } }, + RangeField: { errorKey: 'volume', errors: { volume: 'Volume out of range' } }, + Checkbox: { errorKey: 'terms', errors: { terms: 'You must accept the terms' } }, + CollectionCheckboxes: { errorKey: 'roles', errors: { roles: 'Select at least one role' } }, + CollectionRadioButtons: { errorKey: 'plan', errors: { plan: 'Please select a plan' } }, + Select: { errorKey: 'country', errors: { country: 'Country is required' } }, + MultiSelect: { errorKey: 'languages', errors: { languages: 'Select at least one language' } }, + TextArea: { errorKey: 'bio', errors: { bio: 'Bio is too short' } }, + FileField: { errorKey: 'avatar', errors: { avatar: 'File is required' } }, +} + +export const rubySource: Record = { + TextField: `f.text_field :name, placeholder: "Enter your name"`, + EmailField: `f.email_field :email, placeholder: "you@example.com"`, + PasswordField: `f.password_field :password, placeholder: "Enter password"`, + NumberField: `f.number_field :quantity, in: 0..100`, + DateField: `f.date_field :date, min: "2024-01-01", max: "2025-12-31"`, + DateTimeLocalField: `f.datetime_local_field :starts_at`, + TimeField: `f.time_field :time`, + MonthField: `f.month_field :month, min: "2024-01", max: "2025-12"`, + ColorField: `f.color_field :primary_color`, + SearchField: `f.search_field :q, placeholder: "Search..."`, + TelField: `f.tel_field :phone, placeholder: "+1 (555) 000-0000"`, + UrlField: `f.url_field :website, placeholder: "https://"`, + RangeField: `f.range_field :volume, in: 0..100`, + Checkbox: `f.check_box :terms`, + CollectionCheckboxes: `f.collection_check_boxes :roles, Role.all, :id, :name`, + CollectionRadioButtons: `f.collection_radio_buttons :plan, Plan.all, :id, :name`, + Select: `f.select :country, options_for_select(countries, "us")`, + MultiSelect: `f.select :languages, options_for_select(languages, ["en", "fr"]), {}, { multiple: true }`, + TextArea: `f.text_area :bio, rows: 4, placeholder: "Tell us about yourself..."`, + FileField: `f.file_field :avatar`, + SubmitButton: `f.submit "Save Changes"`, +} + +export const tsUsage: Record = { + TextField: ``, + EmailField: ``, + PasswordField: ``, + NumberField: ``, + DateField: ``, + DateTimeLocalField: ``, + TimeField: ``, + MonthField: ``, + ColorField: ``, + SearchField: ``, + TelField: ``, + UrlField: ``, + RangeField: ``, + Checkbox: ``, + CollectionCheckboxes: ``, + CollectionRadioButtons: ``, + Select: ``, + TextArea: `