diff --git a/.github/workflows/npm-checks.yml b/.github/workflows/npm-checks.yml index c8a7e9c..8120996 100644 --- a/.github/workflows/npm-checks.yml +++ b/.github/workflows/npm-checks.yml @@ -34,8 +34,11 @@ jobs: - name: Install dependencies run: pnpm install + - name: Build showcase + run: pnpm build:showcase-lib + - name: Run lint on all packages - run: pnpm -r lint + run: pnpm lint - name: Run format check on all packages - run: pnpm -r fmt:check + run: pnpm fmt:check diff --git a/.prettierignore b/.prettierignore index 1e07bed..a1edbb2 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,4 +1,5 @@ .github/ +dist/ *.md pnpm-lock.yaml diff --git a/js/react/index.html b/js/react/index.html index a8487ba..bad14f6 100644 --- a/js/react/index.html +++ b/js/react/index.html @@ -5,6 +5,13 @@ RustLangES React Components +
diff --git a/js/react/lib/components/avatar/avatar.showcase.tsx b/js/react/lib/components/avatar/avatar.showcase.tsx new file mode 100644 index 0000000..e1e0dbc --- /dev/null +++ b/js/react/lib/components/avatar/avatar.showcase.tsx @@ -0,0 +1,18 @@ +import { registerCase } from "@rustlanges/showcase"; +import { Avatar } from "./avatar.component"; + +registerCase("Avatar", { + props: { + alt: "string", + size: { + kind: "number", + default: 32, + }, + avatarUrl: { + kind: "string", + default: + "https://images.unsplash.com/photo-1575936123452-b67c3203c357?fm=jpg&q=60&w=3000&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxzZWFyY2h8Mnx8aW1hZ2V8ZW58MHx8MHx8fDA%3D", + }, + }, + component: Avatar, +}); diff --git a/js/react/lib/components/badge/badge.showcase.tsx b/js/react/lib/components/badge/badge.showcase.tsx new file mode 100644 index 0000000..2c0edb8 --- /dev/null +++ b/js/react/lib/components/badge/badge.showcase.tsx @@ -0,0 +1,23 @@ +import { registerCase } from "@rustlanges/showcase"; +import { Badge } from "./badge.component"; + +registerCase("Badge", { + props: { + variant: { + kind: "string", + default: "completed", + options: ["completed", "pending", "reading", "unread"], + }, + type: { + kind: "string", + default: "numeric", + options: ["default", "numeric", "text"], + }, + count: { + kind: "number", + default: 1, + optional: false, + }, + }, + component: Badge, +}); diff --git a/js/react/lib/components/button/button.showcase.tsx b/js/react/lib/components/button/button.showcase.tsx new file mode 100644 index 0000000..f6bb2ad --- /dev/null +++ b/js/react/lib/components/button/button.showcase.tsx @@ -0,0 +1,20 @@ +import { registerCase } from "@rustlanges/showcase"; +import { Button } from "./button.component"; + +registerCase("Button", { + props: { + variant: { + kind: "string", + options: ["primary", "secondary", "text", "icon"], + default: "primary", + }, + label: { + kind: "string", + default: "Botón", + }, + disabled: "boolean", + icon: "icon", + onClick: "callback", + }, + component: Button, +}); diff --git a/js/react/lib/components/calendar/calendar.showcase.tsx b/js/react/lib/components/calendar/calendar.showcase.tsx new file mode 100644 index 0000000..270b3e2 --- /dev/null +++ b/js/react/lib/components/calendar/calendar.showcase.tsx @@ -0,0 +1,18 @@ +import { useState } from "react"; +import { registerCase } from "@rustlanges/showcase"; +import { Calendar } from "./calendar.component"; +import { CalendarRangeDate } from "./calendar.types"; + +registerCase("Calendar", () => { + const [single, setSingle] = useState(new Date()); + const [multiple, setMultiple] = useState | null>(null); + const [range, setRange] = useState(null); + + return ( +
+ + + +
+ ); +}); diff --git a/js/react/lib/components/card/card.showcase.tsx b/js/react/lib/components/card/card.showcase.tsx new file mode 100644 index 0000000..7383007 --- /dev/null +++ b/js/react/lib/components/card/card.showcase.tsx @@ -0,0 +1,15 @@ +import { registerCase } from "@rustlanges/showcase"; +import { Card } from "./card.component"; + +registerCase("Card", { + props: { + clickable: "boolean", + disabled: "boolean", + className: { + kind: "string", + default: "min-w-50 min-h-50", + }, + onClick: "callback", + }, + component: Card, +}); diff --git a/js/react/lib/components/chip/chip.showcase.tsx b/js/react/lib/components/chip/chip.showcase.tsx new file mode 100644 index 0000000..0ab44af --- /dev/null +++ b/js/react/lib/components/chip/chip.showcase.tsx @@ -0,0 +1,17 @@ +import { registerCase } from "@rustlanges/showcase"; +import { Chip } from "./chip.component"; + +registerCase("Chip", { + props: { + variant: { + kind: "string", + default: "featured", + options: ["featured", "numeric", "description", "location", "small"], + }, + label: { + kind: "string", + default: "Destacado", + }, + }, + component: Chip, +}); diff --git a/js/react/lib/components/collaborators/collaborators.showcase.tsx b/js/react/lib/components/collaborators/collaborators.showcase.tsx new file mode 100644 index 0000000..c732d76 --- /dev/null +++ b/js/react/lib/components/collaborators/collaborators.showcase.tsx @@ -0,0 +1,46 @@ +import { registerCase } from "@rustlanges/showcase"; +import { Collaborators } from "./collaborators.component"; + +const collaborator = { + avatarUrl: + "https://images.unsplash.com/photo-1575936123452-b67c3203c357?fm=jpg&q=60&w=3000&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxzZWFyY2h8Mnx8aW1hZ2V8ZW58MHx8MHx8fDA%3D", + nickname: "Colaborador", +}; + +registerCase("Collaborators", () => { + return ( +
+
+ +
+
+ +
+
+ +
+
+ ); +}); diff --git a/js/react/lib/components/contact-form/contact-form.showcase.tsx b/js/react/lib/components/contact-form/contact-form.showcase.tsx new file mode 100644 index 0000000..ac5d370 --- /dev/null +++ b/js/react/lib/components/contact-form/contact-form.showcase.tsx @@ -0,0 +1,6 @@ +import { registerCase } from "@rustlanges/showcase"; +import { ContactForm } from "./contact-form.component"; + +registerCase("Contact Form", () => { + return ; +}); diff --git a/js/react/lib/components/dropdown-tree/dropdown-tree.showcase.tsx b/js/react/lib/components/dropdown-tree/dropdown-tree.showcase.tsx new file mode 100644 index 0000000..42c6f2a --- /dev/null +++ b/js/react/lib/components/dropdown-tree/dropdown-tree.showcase.tsx @@ -0,0 +1,206 @@ +import { registerCase } from "@rustlanges/showcase"; +import { Fragment } from "react/jsx-runtime"; +import { DropdownTree } from "."; + +const tree = { + title: "Introducción a Rust", + state: "completed" as const, + level: "n1" as const, + subjects: [ + { + title: "Aprende lo básico", + state: "completed" as const, + level: "n1" as const, + topics: [ + { + title: "Sintaxis básica", + state: "completed" as const, + level: "n1" as const, + subtopics: [ + { + title: "Variables y declaraciones", + state: "completed" as const, + level: "n1" as const, + }, + { + title: "Constantes y variables estáticas", + state: "completed" as const, + level: "n1" as const, + }, + { + title: "Shadowing", + state: "completed" as const, + level: "n1" as const, + }, + { + title: "Control de flujo", + state: "completed" as const, + level: "n1" as const, + }, + ], + }, + { + title: "Ownership y Borrowing", + state: "completed" as const, + level: "n1" as const, + }, + { + title: "Tipos de datos primitivos", + state: "completed" as const, + level: "n1" as const, + }, + { + title: "Tipos de datos complejos", + state: "completed" as const, + level: "n1" as const, + }, + ], + }, + { + title: "Manejo de errores", + state: "completed" as const, + level: "n2" as const, + topics: [], + }, + { + title: "Cargo", + state: "completed" as const, + level: "n1" as const, + topics: [], + }, + { + title: "Traits", + state: "completed" as const, + level: "n1" as const, + topics: [], + }, + { + title: "Punteros inteligentes", + state: "completed" as const, + level: "n2" as const, + topics: [], + }, + { + title: "Concurrencia y Paralelismo", + state: "completed" as const, + level: "n2" as const, + topics: [], + }, + { + title: "Interoperabilidad", + state: "completed" as const, + level: "op" as const, + topics: [], + }, + { + title: "Ecosistemas y librerías", + state: "completed" as const, + level: "op" as const, + topics: [], + }, + ], +}; + +registerCase("Dropdown Tree", () => { + return ( +
+
+ + {tree.subjects.map(subject => ( + + {subject.topics.map(topic => ( + + {topic.subtopics?.map(subtopic => ( + + ))} + + ))} + + ))} + + + Conoce todos nuestros proyectos Open Source en los + que puedes contribuir y potenciar tu aprendizaje 🚀 + +
+ +
+ + {tree.subjects.map(subject => ( + + {subject.topics.map(topic => ( + + {topic.subtopics?.map(subtopic => ( + + ))} + + ))} + + ))} + + + {tree.subjects.map(subject => ( + + {subject.topics.map(topic => ( + + {topic.subtopics?.map(subtopic => ( + + ))} + + ))} + + ))} + + + Conoce todos nuestros proyectos Open Source en los + que puedes contribuir y potenciar tu aprendizaje 🚀 + +
+
+ ); +}); diff --git a/js/react/lib/components/dropdown/dropdown.showcase.tsx b/js/react/lib/components/dropdown/dropdown.showcase.tsx new file mode 100644 index 0000000..967e52a --- /dev/null +++ b/js/react/lib/components/dropdown/dropdown.showcase.tsx @@ -0,0 +1,14 @@ +import { registerCase } from "@rustlanges/showcase"; +import { DropdownState } from "./dropdown.component"; + +registerCase("Dropdown", { + props: { + onChange: "callback", + value: { + kind: "string", + default: "completed", + options: ["completed", "pending", "reading", "unread"], + }, + }, + component: DropdownState, +}); diff --git a/js/react/lib/components/flap/flap.showcase.tsx b/js/react/lib/components/flap/flap.showcase.tsx new file mode 100644 index 0000000..3903546 --- /dev/null +++ b/js/react/lib/components/flap/flap.showcase.tsx @@ -0,0 +1,17 @@ +import { registerCase } from "@rustlanges/showcase"; +import { Flap } from "./flap.component"; + +registerCase("Flap", { + props: { + variant: { + kind: "string", + default: "descriptive", + options: ["highlight", "descriptive", "numeric"], + }, + label: { + kind: "string", + default: "Oficial", + }, + }, + component: Flap, +}); diff --git a/js/react/lib/components/input-search/input-search.showcase.tsx b/js/react/lib/components/input-search/input-search.showcase.tsx new file mode 100644 index 0000000..0d6c5fe --- /dev/null +++ b/js/react/lib/components/input-search/input-search.showcase.tsx @@ -0,0 +1,38 @@ +import { registerCase } from "@rustlanges/showcase"; +import { InputSearch } from "./input-search.component"; +import { useState } from "react"; + +const filters = [ + { label: "Reciente", value: "reciente" }, + { label: "ES", value: "es" }, + { label: "EN", value: "en" }, + { label: "Libros", value: "libros" }, + { label: "Guías", value: "guias" }, + { label: "Frameworks", value: "frameworks" }, + { label: "Librerías", value: "librerias" }, + { label: "Multinivel", value: "multinivel" }, + { label: "Principiante", value: "principiante" }, + { label: "Intermedio", value: "intermedio" }, + { label: "Avanzado", value: "avanzado" }, +]; + +registerCase("Input Search", () => { + const [activeFilters, setActiveFilters] = useState< + Array<{ + label: string; + value: string; + }> + >([]); + + return ( +
+ + +
+ ); +}); diff --git a/js/react/lib/components/input/input.showcase.tsx b/js/react/lib/components/input/input.showcase.tsx new file mode 100644 index 0000000..0a1a70c --- /dev/null +++ b/js/react/lib/components/input/input.showcase.tsx @@ -0,0 +1,19 @@ +import { registerCase } from "@rustlanges/showcase"; +import { Input } from "./input"; + +registerCase("Input", { + props: { + placeholder: { + kind: "string", + default: "Input", + }, + icon: "icon", + disabled: "boolean", + hasError: "boolean", + errorMessage: { + kind: "string", + default: "Error", + }, + }, + component: Input, +}); diff --git a/js/react/lib/components/level/level.showcase.tsx b/js/react/lib/components/level/level.showcase.tsx new file mode 100644 index 0000000..5a7e98f --- /dev/null +++ b/js/react/lib/components/level/level.showcase.tsx @@ -0,0 +1,22 @@ +import { registerCase } from "@rustlanges/showcase"; +import { Level } from "./level.component"; + +registerCase("Level", { + props: { + variant: { + kind: "string", + options: ["n1", "n2", "n3", "op"], + default: "n1", + }, + label: { + kind: "string", + default: "Oficial", + }, + as: { + kind: "string", + default: "span", + options: ["a", "button", "span"], + }, + }, + component: Level, +}); diff --git a/js/react/lib/components/progress-bar/progress-bar.showcase.tsx b/js/react/lib/components/progress-bar/progress-bar.showcase.tsx new file mode 100644 index 0000000..512151b --- /dev/null +++ b/js/react/lib/components/progress-bar/progress-bar.showcase.tsx @@ -0,0 +1,13 @@ +import { registerCase } from "@rustlanges/showcase"; +import { ProgressBar } from "./progress-bar.component"; + +registerCase("Progress Bar", { + props: { + percentage: { + kind: "number", + default: 50, + optional: false, + }, + }, + component: ProgressBar, +}); diff --git a/js/react/lib/components/radio/radio.showcase.tsx b/js/react/lib/components/radio/radio.showcase.tsx new file mode 100644 index 0000000..411fe3f --- /dev/null +++ b/js/react/lib/components/radio/radio.showcase.tsx @@ -0,0 +1,12 @@ +import { registerCase } from "@rustlanges/showcase"; +import { Radio } from "./radio.component"; + +registerCase("Radio", { + props: { + checked: { + kind: "boolean", + optional: true, + }, + }, + component: Radio, +}); diff --git a/js/react/lib/components/tag/tag.showcase.tsx b/js/react/lib/components/tag/tag.showcase.tsx new file mode 100644 index 0000000..ddd9b8a --- /dev/null +++ b/js/react/lib/components/tag/tag.showcase.tsx @@ -0,0 +1,20 @@ +import { registerCase } from "@rustlanges/showcase"; +import { Tag } from "./tag.component"; + +registerCase("Tag", { + props: { + label: { + kind: "string", + default: "tag", + }, + selected: "boolean", + as: { + kind: "string", + optional: true, + options: ["span", "button", "a"], + }, + + onClick: "callback", + }, + component: Tag, +}); diff --git a/js/react/lib/showcases.ts b/js/react/lib/showcases.ts new file mode 100644 index 0000000..104c910 --- /dev/null +++ b/js/react/lib/showcases.ts @@ -0,0 +1,17 @@ +import "./components/avatar/avatar.showcase"; +import "./components/badge/badge.showcase"; +import "./components/button/button.showcase"; +import "./components/calendar/calendar.showcase"; +import "./components/card/card.showcase"; +import "./components/chip/chip.showcase"; +import "./components/collaborators/collaborators.showcase"; +import "./components/contact-form/contact-form.showcase"; +import "./components/dropdown/dropdown.showcase"; +import "./components/dropdown-tree/dropdown-tree.showcase"; +import "./components/flap/flap.showcase"; +import "./components/input/input.showcase"; +import "./components/input-search/input-search.showcase"; +import "./components/level/level.showcase"; +import "./components/progress-bar/progress-bar.showcase"; +import "./components/radio/radio.showcase"; +import "./components/tag/tag.showcase"; diff --git a/js/react/package.json b/js/react/package.json index 4decec3..77dce2a 100644 --- a/js/react/package.json +++ b/js/react/package.json @@ -10,6 +10,9 @@ }, "./styles.css": { "default": "./dist/styles.css" + }, + "./dev": { + "default": "./lib/index.ts" } }, "files": [ @@ -22,7 +25,8 @@ }, "scripts": { "dev": "vite", - "build": "tsc -b ./tsconfig.lib.json && vite build && npm run check:tsc && npm run build:tailwindcss", + "build": "tsc -b ./tsconfig.lib.json && vite build && pnpm run check:tsc && pnpm run build:tailwindcss", + "build:showcase": "BUILD_SHOWCASE=1 NODE_ENV=development pnpm run build", "build:tailwindcss": "npx @tailwindcss/cli -i lib/styles.css -o dist/styles.css --minify", "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0 && npm run check:tsc", "check:tsc": "tsc --noEmit -p tsconfig.lib.json", @@ -35,6 +39,7 @@ "devDependencies": { "@eslint/js": "^9.28.0", "@rustlanges/styles": "file:../../styles", + "@rustlanges/showcase": "file:../showcase", "@tailwindcss/cli": "^4.1.8", "@tailwindcss/vite": "^4.1.8", "@types/node": "^22.15.29", diff --git a/js/react/showcase/App.tsx b/js/react/showcase/App.tsx deleted file mode 100644 index 973d45e..0000000 --- a/js/react/showcase/App.tsx +++ /dev/null @@ -1,578 +0,0 @@ -import { - Button, - ContactForm, - Example, - Github, - Tag, - Telegram, - Input, - Location, - Flap, - Chip, - Level, - Collaborators, - Radio, - Badge, - DropdownState, - Card, - Calendar, - CalendarRangeDate, - DropdownTree, - ProgressBar, - InputSearch, -} from "@rustlanges/react"; -import { ShowComponent } from "./ShowComponent"; -import { Fragment, useState } from "react"; - -const collaborator = { - avatarUrl: - "https://images.unsplash.com/photo-1575936123452-b67c3203c357?fm=jpg&q=60&w=3000&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxzZWFyY2h8Mnx8aW1hZ2V8ZW58MHx8MHx8fDA%3D", - nickname: "Colaborador", -}; - -const tree = { - title: "Introducción a Rust", - state: "completed" as const, - level: "n1" as const, - subjects: [ - { - title: "Aprende lo básico", - state: "completed" as const, - level: "n1" as const, - topics: [ - { - title: "Sintaxis básica", - state: "completed" as const, - level: "n1" as const, - subtopics: [ - { - title: "Variables y declaraciones", - state: "completed" as const, - level: "n1" as const, - }, - { - title: "Constantes y variables estáticas", - state: "completed" as const, - level: "n1" as const, - }, - { - title: "Shadowing", - state: "completed" as const, - level: "n1" as const, - }, - { - title: "Control de flujo", - state: "completed" as const, - level: "n1" as const, - }, - ], - }, - { - title: "Ownership y Borrowing", - state: "completed" as const, - level: "n1" as const, - }, - { - title: "Tipos de datos primitivos", - state: "completed" as const, - level: "n1" as const, - }, - { - title: "Tipos de datos complejos", - state: "completed" as const, - level: "n1" as const, - }, - ], - }, - { - title: "Manejo de errores", - state: "completed" as const, - level: "n2" as const, - topics: [], - }, - { - title: "Cargo", - state: "completed" as const, - level: "n1" as const, - topics: [], - }, - { - title: "Traits", - state: "completed" as const, - level: "n1" as const, - topics: [], - }, - { - title: "Punteros inteligentes", - state: "completed" as const, - level: "n2" as const, - topics: [], - }, - { - title: "Concurrencia y Paralelismo", - state: "completed" as const, - level: "n2" as const, - topics: [], - }, - { - title: "Interoperabilidad", - state: "completed" as const, - level: "op" as const, - topics: [], - }, - { - title: "Ecosistemas y librerías", - state: "completed" as const, - level: "op" as const, - topics: [], - }, - ], -}; - -const filters = [ - { label: "Reciente", value: "reciente" }, - { label: "ES", value: "es" }, - { label: "EN", value: "en" }, - { label: "Libros", value: "libros" }, - { label: "Guías", value: "guias" }, - { label: "Frameworks", value: "frameworks" }, - { label: "Librerías", value: "librerias" }, - { label: "Multinivel", value: "multinivel" }, - { label: "Principiante", value: "principiante" }, - { label: "Intermedio", value: "intermedio" }, - { label: "Avanzado", value: "avanzado" }, -]; - -export function App() { - const [single, setSingle] = useState(new Date()); - const [multiple, setMultiple] = useState | null>(null); - const [range, setRange] = useState(null); - const [activeFilters, setActiveFilters] = useState< - Array<{ - label: string; - value: string; - }> - >([]); - - return ( -
-

- RustLangES Components -

-

- Change your computer theme to explore the different styles (light, dark) -

- - - - -
- ); -} diff --git a/js/react/showcase/ErrorBoundary.ts b/js/react/showcase/ErrorBoundary.ts deleted file mode 100644 index 8ac98b7..0000000 --- a/js/react/showcase/ErrorBoundary.ts +++ /dev/null @@ -1,23 +0,0 @@ -import React, { Component, PropsWithChildren } from "react"; - -export class ErrorBoundary extends Component< - PropsWithChildren<{ fallback(err: Error): React.ReactNode }>, - { error?: Error } -> { - componentDidCatch(error: Error): void { - console.error(error); - this.setState({ - error, - }); - } - - static getDerivedStateFromError(error: Error) { - return { error }; - } - - render(): React.ReactNode { - return this.state?.error - ? this.props.fallback(this.state.error) - : this.props.children; - } -} diff --git a/js/react/showcase/ShowComponent/Container.tsx b/js/react/showcase/ShowComponent/Container.tsx deleted file mode 100644 index bf477bc..0000000 --- a/js/react/showcase/ShowComponent/Container.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import { PropsWithChildren } from "react"; - -import { ChevronDown } from "../icons"; - -export function ShowComponentContainer( - props: PropsWithChildren<{ - title: React.ReactNode; - className?: string; - contentClassName?: string; - }> -) { - return ( -
- - {props.title} - - - - -
- {props.children} -
-
- ); -} diff --git a/js/react/showcase/ShowComponent/Error.tsx b/js/react/showcase/ShowComponent/Error.tsx deleted file mode 100644 index 5ba60f2..0000000 --- a/js/react/showcase/ShowComponent/Error.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import { BombIcon } from "../icons"; -import { ShowComponentContainer } from "./Container"; - -export function ShowComponentError({ - title, - error: err, -}: { - title: string; - error: Error; -}) { - return ( - - {title} - - } - className="bg-red-300" - contentClassName="flex-col" - > - {err.message} - {(err.stack?.split?.("\n") ?? []).map(line => { - // really last @ - const [, name, source_ = ""] = line.match(/(.*)@([^@]*)$/) ?? [, line]; - - let source = source_; - - if ( - source.startsWith("vite/client") || - source.startsWith("react-refresh") || - name.includes("/node_modules/") || - name.startsWith("__require") || - name.trim().length === 0 - ) { - return; - } - - source = source.startsWith(location.origin) - ? source.substring(location.origin.length) - : source; - - // match to ?:: - const [, sourceFile, lineN, columnN] = source.match( - /^(.+)?(?:t=\d+|v=\w+):(\d+):(\d+)$/ - ) ?? [, source, "", ""]; - source = `${sourceFile}${lineN}:${columnN}`; - - return ( - - {name} - - {source} - - - ); - })} - - ); -} diff --git a/js/react/showcase/ShowComponent/Field.tsx b/js/react/showcase/ShowComponent/Field.tsx deleted file mode 100644 index dd95fb0..0000000 --- a/js/react/showcase/ShowComponent/Field.tsx +++ /dev/null @@ -1,134 +0,0 @@ -import { useEffect } from "react"; -import { NormalizedProps } from "./types"; - -export function ShowComponentField({ - name, - def, - setValue, - modified, - setModified, - ...props -}: { - name: string; - def: NormalizedProps[string]; - value: unknown; - setValue(v: unknown): void; - modified: boolean; - setModified(v: boolean): void; -}) { - const value = !def.optional || modified ? props.value : def.default; - - useEffect(() => { - if (def.type === "callback" && value) { - const timer = setTimeout(() => setValue(false), 100); - return () => clearTimeout(timer); - } - }); - - return ( -
- - {modified && ( - - )} - {def.type === "string" ? ( - <> - {def.options.length ? ( - - ) : ( -