diff --git a/docs/platform-native.md b/docs/platform-native.md new file mode 100644 index 0000000..3770f92 --- /dev/null +++ b/docs/platform-native.md @@ -0,0 +1,173 @@ +# Platform-Native Solutions + +The lazy senior dev's first question is always: *does the platform already do this?* + +This document answers that question for the most common cases. Before reaching for a package, scan here. The platform ships with your app for free, doesn't break on updates, and was written by people whose job is exactly that problem. + +--- + +## HTML Elements + +Things the browser already has as a form control. + +| You think you need | What the platform has | +|---|---| +| Date picker library | `` | +| Time picker library | `` | +| Color picker library | `` | +| Range slider library | `` | +| Progress bar component | `` | +| Meter/gauge component | `` | +| Modal/dialog library | `` + `dialog.showModal()` | +| Accordion/FAQ component | `
Title
` | +| Tooltip library | `title` attribute + CSS `::before`/`::after` | +| Searchable dropdown | ` ` | +| Auto-growing textarea | `field-sizing: content` (CSS) | +| Sticky header | `position: sticky; top: 0` (CSS) | + +--- + +## CSS Capabilities + +Things developers reach for JavaScript to do. + +| You think you need JS for | What CSS has | +|---|---| +| Responsive font size | `font-size: clamp(1rem, 2.5vw, 2rem)` | +| Fluid spacing | `padding: clamp(1rem, 5vw, 3rem)` | +| Dark mode | `@media (prefers-color-scheme: dark)` | +| Reduced motion | `@media (prefers-reduced-motion: reduce)` | +| Responsive layout without breakpoints | `grid-template-columns: repeat(auto-fill, minmax(250px, 1fr))` | +| Component-level responsive design | `@container` queries | +| Global design tokens / theming | CSS custom properties (`--color-primary: #7c3aed`) | +| Smooth scroll | `scroll-behavior: smooth` | +| Scroll-snap carousel | `scroll-snap-type: x mandatory` + `scroll-snap-align: start` | +| Aspect ratio enforcement | `aspect-ratio: 16 / 9` | +| Truncate text with ellipsis | `overflow: hidden; text-overflow: ellipsis; white-space: nowrap` | +| Multi-line text clamp | `-webkit-line-clamp: 3` | +| CSS cascade layers (style isolation) | `@layer base, components, utilities` | +| Nested CSS selectors | Native CSS nesting (no preprocessor needed) | +| `has()` parent selector | `:has(input:checked)` | + +--- + +## JavaScript / Browser APIs + +Libraries people install that the runtime already ships. + +| You think you need | What the platform has | +|---|---| +| `query-string` / `qs` | `new URLSearchParams(location.search)` | +| `lodash.clonedeep` | `structuredClone(obj)` | +| `lodash.groupby` | `Object.groupBy(arr, fn)` | +| `lodash.debounce` | — see debounce one-liner below | +| `numeral` / `accounting` | `new Intl.NumberFormat("en-US", { style: "currency", currency: "USD" })` | +| `date-fns` format | `new Intl.DateTimeFormat("en-US", { dateStyle: "long" }).format(date)` | +| `date-fns` relative time | `new Intl.RelativeTimeFormat("en", { numeric: "auto" }).format(-3, "day")` | +| `plural` / `i18n` plurals | `new Intl.PluralRules("en-US").select(count)` | +| `clipboard.js` | `navigator.clipboard.writeText(text)` | +| `uuid` (v4) | `crypto.randomUUID()` | +| Infinite scroll library | `new IntersectionObserver(cb).observe(sentinel)` | +| Resize listener library | `new ResizeObserver(cb).observe(element)` | +| DOM mutation watcher | `new MutationObserver(cb).observe(el, options)` | +| `uuid-validate` | `/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(id)` | +| `is-online` / `connectivity check` | `navigator.onLine` + `online`/`offline` events | +| `sharesheet` library | `navigator.share({ title, text, url })` | +| `store.js` / `localForage` (simple case) | `localStorage.setItem(key, JSON.stringify(val))` | +| Abort fetch on timeout | `AbortSignal.timeout(5000)` passed to `fetch` | +| Custom event bus | `new EventTarget()` / `dispatchEvent(new CustomEvent("x", { detail }))` | + +**Debounce one-liner** (no library): +```js +// ponytail: 3 lines beats a dependency +let t; +const debounce = (fn, ms) => (...args) => { clearTimeout(t); t = setTimeout(() => fn(...args), ms); }; +``` + +--- + +## Node.js Standard Library + +Packages that wrap Node built-ins. + +| You think you need | What Node has | +|---|---| +| `mkdirp` | `fs.mkdirSync(path, { recursive: true })` | +| `rimraf` | `fs.rmSync(path, { recursive: true, force: true })` | +| `make-dir` | `fs.mkdirSync(path, { recursive: true })` | +| `slash` (win paths) | `path.posix` or `path.normalize()` | +| `uuid` (v4) | `crypto.randomUUID()` | +| `ms` (parse duration strings) | — keep `ms`, it's genuinely useful and tiny | +| `is-stream` | `val instanceof stream.Readable` | +| `object-assign` | `Object.assign()` / spread | +| `array-uniq` | `[...new Set(arr)]` | +| `array-flatten` | `arr.flat(Infinity)` | +| `flat` | `arr.flat(depth)` | +| `path-exists` | `fs.existsSync(path)` | +| `load-json-file` | `JSON.parse(fs.readFileSync(path, "utf8"))` | +| `write-json-file` | `fs.writeFileSync(path, JSON.stringify(obj, null, 2))` | +| `pkg-dir` | `path.resolve(__dirname, "..")` / `import.meta.dirname` | + +--- + +## Python Standard Library + +Packages that wrap what Python already ships. + +| You think you need | What Python has | +|---|---| +| `python-dateutil` (basic parsing) | `datetime.fromisoformat()` (Python 3.7+) | +| `pytz` | `zoneinfo.ZoneInfo("America/New_York")` (Python 3.9+) | +| `attrs` (simple data classes) | `@dataclass` | +| `six` | — drop it, Python 2 is gone | +| `pathlib2` | `pathlib.Path` (built-in since Python 3.4) | +| `enum34` | `enum.Enum` (built-in since Python 3.4) | +| `typing_extensions` (common types) | `from __future__ import annotations` + built-in generics | +| `simplejson` (basic use) | `json` (stdlib) | +| `requests` (simple GET) | `urllib.request.urlopen(url)` — `requests` for anything real | +| `click` (single command) | `argparse` (stdlib) | +| `mergedeep` | `dict \| other_dict` (Python 3.9+) | +| `more-itertools` (basic) | `itertools` (stdlib): `chain`, `islice`, `groupby`, `product` | +| `toolz` (basic) | `functools`: `lru_cache`, `partial`, `reduce` | +| `tabulate` (dev/debug only) | `pprint.pprint()` for quick inspection | + +--- + +## Database + +Things the application layer implements that the database already does. + +| You think you need app code for | What the database has | +|---|---| +| Pagination offset/limit | `LIMIT 20 OFFSET 40` | +| Running totals | `SUM(...) OVER (ORDER BY date)` (window function) | +| Rank within group | `RANK() OVER (PARTITION BY category ORDER BY score DESC)` | +| Pivot / cross-tab | `FILTER (WHERE ...)` + conditional aggregation | +| Deduplication | `SELECT DISTINCT` / `ON CONFLICT DO NOTHING` | +| Soft-delete filtering | Generated column + partial index | +| Tree traversal | Recursive CTE (`WITH RECURSIVE`) | +| Full-text search (basic) | `tsvector` / `MATCH AGAINST` / `FTS5` | +| JSON storage + query | `jsonb` (Postgres) / `JSON_EXTRACT` (SQLite/MySQL) | +| UUID generation | `gen_random_uuid()` (Postgres) / `UUID()` (MySQL) | +| Timestamps on insert/update | `DEFAULT now()` + trigger or `ON UPDATE CURRENT_TIMESTAMP` | +| Enforce uniqueness | `UNIQUE` constraint — not application-level checks | +| Enforce referential integrity | `FOREIGN KEY` — not application-level checks | +| Enforce value ranges | `CHECK (price > 0)` — not application-level validation | + +--- + +## The Pattern + +Across every layer, the pattern is the same: + +``` +Platform team spends years solving the problem. +Package author wraps it. +You install the wrapper. +The wrapper goes unmaintained. +You debug the wrapper. +``` + +Skip the wrapper. The platform ships with your app for free. + +When the native solution is genuinely insufficient (old browser support, edge cases it doesn't handle, ergonomics that matter at scale), the library earns its place. Install it then — not before. diff --git a/examples/deep-clone.md b/examples/deep-clone.md new file mode 100644 index 0000000..89cbf62 --- /dev/null +++ b/examples/deep-clone.md @@ -0,0 +1,31 @@ +# Deep Clone + +**Task:** "Deep clone this object." + +## Without Ponytail + +```bash +npm install lodash +``` + +```js +import { cloneDeep } from "lodash"; + +const copy = cloneDeep(original); +``` + +Or the classic hack: + +```js +// fragile: loses Date, undefined, Map, Set, circular refs, functions +const copy = JSON.parse(JSON.stringify(original)); +``` + +## With Ponytail + +```js +// ponytail: structuredClone does this +const copy = structuredClone(original); +``` + +**1 dependency (or a fragile hack) → 1 built-in.** `structuredClone` handles `Date`, `Map`, `Set`, `ArrayBuffer`, `RegExp`, circular references, and more — everything `JSON.parse/stringify` silently drops. Available in every browser since 2022 and Node.js since v17. Pull lodash in when you need the rest of it, not for one function. diff --git a/examples/group-by.md b/examples/group-by.md new file mode 100644 index 0000000..71e92da --- /dev/null +++ b/examples/group-by.md @@ -0,0 +1,35 @@ +# Group By + +**Task:** "Group this array of objects by a key." + +## Without Ponytail + +```bash +npm install lodash +``` + +```js +import { groupBy } from "lodash"; + +const byStatus = groupBy(orders, "status"); +// → { pending: [...], shipped: [...], delivered: [...] } +``` + +Or the hand-rolled version: + +```js +const byStatus = orders.reduce((acc, order) => { + (acc[order.status] ??= []).push(order); + return acc; +}, {}); +``` + +## With Ponytail + +```js +// ponytail: Object.groupBy does this +const byStatus = Object.groupBy(orders, order => order.status); +// → { pending: [...], shipped: [...], delivered: [...] } +``` + +**1 dependency (or a reduce) → 1 built-in.** `Object.groupBy` shipped in Chrome 117, Firefox 119, Safari 17.4, Node.js 21. If you need a `Map` instead of a plain object: `Map.groupBy(orders, o => o.status)`. Check your target runtime; if you need IE11 or old Node, the `reduce` one-liner is still the right call — not lodash. diff --git a/examples/infinite-scroll.md b/examples/infinite-scroll.md new file mode 100644 index 0000000..321faac --- /dev/null +++ b/examples/infinite-scroll.md @@ -0,0 +1,58 @@ +# Infinite Scroll + +**Task:** "Load more items when the user scrolls to the bottom." + +## Without Ponytail + +```bash +npm install react-infinite-scroll-component +``` + +```jsx +import InfiniteScroll from "react-infinite-scroll-component"; + +export function Feed({ items, fetchMore, hasMore }) { + return ( + } + endMessage={

No more items

} + scrollThreshold={0.9} + > + {items.map(item => )} +
+ ); +} +``` + +A dependency to watch scroll position and fire a callback. + +## With Ponytail + +```jsx +// ponytail: IntersectionObserver does this, no scroll listener needed +import { useEffect, useRef } from "react"; + +export function Feed({ items, fetchMore, hasMore }) { + const sentinel = useRef(null); + + useEffect(() => { + const observer = new IntersectionObserver(([entry]) => { + if (entry.isIntersecting && hasMore) fetchMore(); + }); + if (sentinel.current) observer.observe(sentinel.current); + return () => observer.disconnect(); + }, [hasMore, fetchMore]); + + return ( + <> + {items.map(item => )} +
+ + ); +} +``` + +**1 dependency → 0 dependencies.** `IntersectionObserver` fires only when the sentinel enters the viewport — no scroll event, no throttling, no jank. Ships in every browser. The library wraps exactly this API. diff --git a/examples/modal-dialog.md b/examples/modal-dialog.md new file mode 100644 index 0000000..0c970e9 --- /dev/null +++ b/examples/modal-dialog.md @@ -0,0 +1,62 @@ +# Modal Dialog + +**Task:** "Add a modal dialog for the delete confirmation." + +## Without Ponytail + +```bash +npm install @radix-ui/react-dialog +# or: npm install react-modal +``` + +```jsx +import * as Dialog from "@radix-ui/react-dialog"; +import { useState } from "react"; + +export function DeleteModal({ onConfirm, onCancel }) { + return ( + + + + + + + + Confirm deletion + This action cannot be undone. +
+ + + + +
+
+
+
+ ); +} +``` + +A dependency, a portal, an overlay, a root, a trigger, a content wrapper — to show a box with two buttons. + +## With Ponytail + +```html + + +

This action cannot be undone.

+ + +
+``` + +```js +const dialog = document.getElementById("confirm-delete"); +document.getElementById("cancel").onclick = () => dialog.close(); +document.getElementById("confirm").onclick = () => { onConfirm(); dialog.close(); }; + +// Open it: +dialog.showModal(); +``` + +**1 dependency + 30 lines → 0 dependencies + 8 lines.** The native `` traps focus automatically, closes on Escape, renders a backdrop via `::backdrop`, and is accessible by default. All browsers since 2022. The library was solving a problem the platform solved. diff --git a/examples/number-formatting.md b/examples/number-formatting.md new file mode 100644 index 0000000..4773fa5 --- /dev/null +++ b/examples/number-formatting.md @@ -0,0 +1,37 @@ +# Number Formatting + +**Task:** "Format numbers as currency and with thousand separators." + +## Without Ponytail + +```bash +npm install numeral +# or: npm install accounting +``` + +```js +import numeral from "numeral"; + +numeral(1234567.89).format("$1,234.00"); // "$1,234,567.89" +numeral(0.745).format("0.0%"); // "74.5%" +numeral(1500).format("0.0a"); // "1.5k" +``` + +## With Ponytail + +```js +// ponytail: Intl.NumberFormat does this, locale-aware +new Intl.NumberFormat("en-US", { style: "currency", currency: "USD" }) + .format(1234567.89); +// → "$1,234,567.89" + +new Intl.NumberFormat("en-US", { style: "percent" }) + .format(0.745); +// → "74.5%" + +new Intl.NumberFormat("en-US", { notation: "compact" }) + .format(1500); +// → "1.5K" +``` + +**1 dependency → 0 dependencies.** `Intl.NumberFormat` is built into every JS runtime, handles every locale correctly, and gets currency symbols, decimal separators, and grouping right for any market without a lookup table. A library that hardcodes formats will always be wrong for someone. diff --git a/examples/url-params.md b/examples/url-params.md new file mode 100644 index 0000000..e2ed554 --- /dev/null +++ b/examples/url-params.md @@ -0,0 +1,41 @@ +# URL Parameters + +**Task:** "Parse and build URL query strings." + +## Without Ponytail + +```bash +npm install query-string +# 4.5 kB gzipped, 3.5M downloads/week +``` + +```js +import qs from "query-string"; + +// Parse +const params = qs.parse(location.search); +// → { page: "2", sort: "name", tags: ["js", "css"] } + +// Build +const url = qs.stringify({ page: 2, sort: "name", tags: ["js", "css"] }); +// → "page=2&sort=name&tags=js&tags=css" +``` + +## With Ponytail + +```js +// ponytail: URLSearchParams does this +const params = new URLSearchParams(location.search); + +// Read +params.get("page"); // "2" +params.getAll("tags"); // ["js", "css"] + +// Build +const out = new URLSearchParams({ page: 2, sort: "name" }); +out.append("tags", "js"); +out.append("tags", "css"); +out.toString(); // "page=2&sort=name&tags=js&tags=css" +``` + +**1 dependency → 0 dependencies.** `URLSearchParams` is in every browser and in Node.js since v10. It handles encoding, repeated keys, and iteration. The package was a polyfill for an API that has shipped everywhere for years.