Skip to content

Commit b345e49

Browse files
examples: add 6 new over-engineering survivors + platform-native guide (#109)
New examples (examples/): - modal-dialog: <dialog> vs Radix/react-modal - url-params: URLSearchParams vs query-string - number-formatting: Intl.NumberFormat vs numeral - infinite-scroll: IntersectionObserver vs react-infinite-scroll-component - deep-clone: structuredClone vs lodash.cloneDeep / JSON hack - group-by: Object.groupBy vs lodash.groupBy New doc (docs/platform-native.md): Comprehensive reference of platform-native solutions across HTML elements, CSS, Browser APIs, Node.js stdlib, Python stdlib, and database features. Covers 60+ cases where the platform already has what developers reach for a package to do.
1 parent 7f4dc90 commit b345e49

7 files changed

Lines changed: 437 additions & 0 deletions

File tree

‎docs/platform-native.md‎

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
# Platform-Native Solutions
2+
3+
The lazy senior dev's first question is always: *does the platform already do this?*
4+
5+
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.
6+
7+
---
8+
9+
## HTML Elements
10+
11+
Things the browser already has as a form control.
12+
13+
| You think you need | What the platform has |
14+
|---|---|
15+
| Date picker library | `<input type="date">` |
16+
| Time picker library | `<input type="time">` |
17+
| Color picker library | `<input type="color">` |
18+
| Range slider library | `<input type="range">` |
19+
| Progress bar component | `<progress value="70" max="100">` |
20+
| Meter/gauge component | `<meter value="0.7">` |
21+
| Modal/dialog library | `<dialog>` + `dialog.showModal()` |
22+
| Accordion/FAQ component | `<details><summary>Title</summary>…</details>` |
23+
| Tooltip library | `title` attribute + CSS `::before`/`::after` |
24+
| Searchable dropdown | `<input list="id"> <datalist id="id">` |
25+
| Auto-growing textarea | `field-sizing: content` (CSS) |
26+
| Sticky header | `position: sticky; top: 0` (CSS) |
27+
28+
---
29+
30+
## CSS Capabilities
31+
32+
Things developers reach for JavaScript to do.
33+
34+
| You think you need JS for | What CSS has |
35+
|---|---|
36+
| Responsive font size | `font-size: clamp(1rem, 2.5vw, 2rem)` |
37+
| Fluid spacing | `padding: clamp(1rem, 5vw, 3rem)` |
38+
| Dark mode | `@media (prefers-color-scheme: dark)` |
39+
| Reduced motion | `@media (prefers-reduced-motion: reduce)` |
40+
| Responsive layout without breakpoints | `grid-template-columns: repeat(auto-fill, minmax(250px, 1fr))` |
41+
| Component-level responsive design | `@container` queries |
42+
| Global design tokens / theming | CSS custom properties (`--color-primary: #7c3aed`) |
43+
| Smooth scroll | `scroll-behavior: smooth` |
44+
| Scroll-snap carousel | `scroll-snap-type: x mandatory` + `scroll-snap-align: start` |
45+
| Aspect ratio enforcement | `aspect-ratio: 16 / 9` |
46+
| Truncate text with ellipsis | `overflow: hidden; text-overflow: ellipsis; white-space: nowrap` |
47+
| Multi-line text clamp | `-webkit-line-clamp: 3` |
48+
| CSS cascade layers (style isolation) | `@layer base, components, utilities` |
49+
| Nested CSS selectors | Native CSS nesting (no preprocessor needed) |
50+
| `has()` parent selector | `:has(input:checked)` |
51+
52+
---
53+
54+
## JavaScript / Browser APIs
55+
56+
Libraries people install that the runtime already ships.
57+
58+
| You think you need | What the platform has |
59+
|---|---|
60+
| `query-string` / `qs` | `new URLSearchParams(location.search)` |
61+
| `lodash.clonedeep` | `structuredClone(obj)` |
62+
| `lodash.groupby` | `Object.groupBy(arr, fn)` |
63+
| `lodash.debounce` | — see debounce one-liner below |
64+
| `numeral` / `accounting` | `new Intl.NumberFormat("en-US", { style: "currency", currency: "USD" })` |
65+
| `date-fns` format | `new Intl.DateTimeFormat("en-US", { dateStyle: "long" }).format(date)` |
66+
| `date-fns` relative time | `new Intl.RelativeTimeFormat("en", { numeric: "auto" }).format(-3, "day")` |
67+
| `plural` / `i18n` plurals | `new Intl.PluralRules("en-US").select(count)` |
68+
| `clipboard.js` | `navigator.clipboard.writeText(text)` |
69+
| `uuid` (v4) | `crypto.randomUUID()` |
70+
| Infinite scroll library | `new IntersectionObserver(cb).observe(sentinel)` |
71+
| Resize listener library | `new ResizeObserver(cb).observe(element)` |
72+
| DOM mutation watcher | `new MutationObserver(cb).observe(el, options)` |
73+
| `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)` |
74+
| `is-online` / `connectivity check` | `navigator.onLine` + `online`/`offline` events |
75+
| `sharesheet` library | `navigator.share({ title, text, url })` |
76+
| `store.js` / `localForage` (simple case) | `localStorage.setItem(key, JSON.stringify(val))` |
77+
| Abort fetch on timeout | `AbortSignal.timeout(5000)` passed to `fetch` |
78+
| Custom event bus | `new EventTarget()` / `dispatchEvent(new CustomEvent("x", { detail }))` |
79+
80+
**Debounce one-liner** (no library):
81+
```js
82+
// ponytail: 3 lines beats a dependency
83+
let t;
84+
const debounce = (fn, ms) => (...args) => { clearTimeout(t); t = setTimeout(() => fn(...args), ms); };
85+
```
86+
87+
---
88+
89+
## Node.js Standard Library
90+
91+
Packages that wrap Node built-ins.
92+
93+
| You think you need | What Node has |
94+
|---|---|
95+
| `mkdirp` | `fs.mkdirSync(path, { recursive: true })` |
96+
| `rimraf` | `fs.rmSync(path, { recursive: true, force: true })` |
97+
| `make-dir` | `fs.mkdirSync(path, { recursive: true })` |
98+
| `slash` (win paths) | `path.posix` or `path.normalize()` |
99+
| `uuid` (v4) | `crypto.randomUUID()` |
100+
| `ms` (parse duration strings) | — keep `ms`, it's genuinely useful and tiny |
101+
| `is-stream` | `val instanceof stream.Readable` |
102+
| `object-assign` | `Object.assign()` / spread |
103+
| `array-uniq` | `[...new Set(arr)]` |
104+
| `array-flatten` | `arr.flat(Infinity)` |
105+
| `flat` | `arr.flat(depth)` |
106+
| `path-exists` | `fs.existsSync(path)` |
107+
| `load-json-file` | `JSON.parse(fs.readFileSync(path, "utf8"))` |
108+
| `write-json-file` | `fs.writeFileSync(path, JSON.stringify(obj, null, 2))` |
109+
| `pkg-dir` | `path.resolve(__dirname, "..")` / `import.meta.dirname` |
110+
111+
---
112+
113+
## Python Standard Library
114+
115+
Packages that wrap what Python already ships.
116+
117+
| You think you need | What Python has |
118+
|---|---|
119+
| `python-dateutil` (basic parsing) | `datetime.fromisoformat()` (Python 3.7+) |
120+
| `pytz` | `zoneinfo.ZoneInfo("America/New_York")` (Python 3.9+) |
121+
| `attrs` (simple data classes) | `@dataclass` |
122+
| `six` | — drop it, Python 2 is gone |
123+
| `pathlib2` | `pathlib.Path` (built-in since Python 3.4) |
124+
| `enum34` | `enum.Enum` (built-in since Python 3.4) |
125+
| `typing_extensions` (common types) | `from __future__ import annotations` + built-in generics |
126+
| `simplejson` (basic use) | `json` (stdlib) |
127+
| `requests` (simple GET) | `urllib.request.urlopen(url)` — `requests` for anything real |
128+
| `click` (single command) | `argparse` (stdlib) |
129+
| `mergedeep` | `dict \| other_dict` (Python 3.9+) |
130+
| `more-itertools` (basic) | `itertools` (stdlib): `chain`, `islice`, `groupby`, `product` |
131+
| `toolz` (basic) | `functools`: `lru_cache`, `partial`, `reduce` |
132+
| `tabulate` (dev/debug only) | `pprint.pprint()` for quick inspection |
133+
134+
---
135+
136+
## Database
137+
138+
Things the application layer implements that the database already does.
139+
140+
| You think you need app code for | What the database has |
141+
|---|---|
142+
| Pagination offset/limit | `LIMIT 20 OFFSET 40` |
143+
| Running totals | `SUM(...) OVER (ORDER BY date)` (window function) |
144+
| Rank within group | `RANK() OVER (PARTITION BY category ORDER BY score DESC)` |
145+
| Pivot / cross-tab | `FILTER (WHERE ...)` + conditional aggregation |
146+
| Deduplication | `SELECT DISTINCT` / `ON CONFLICT DO NOTHING` |
147+
| Soft-delete filtering | Generated column + partial index |
148+
| Tree traversal | Recursive CTE (`WITH RECURSIVE`) |
149+
| Full-text search (basic) | `tsvector` / `MATCH AGAINST` / `FTS5` |
150+
| JSON storage + query | `jsonb` (Postgres) / `JSON_EXTRACT` (SQLite/MySQL) |
151+
| UUID generation | `gen_random_uuid()` (Postgres) / `UUID()` (MySQL) |
152+
| Timestamps on insert/update | `DEFAULT now()` + trigger or `ON UPDATE CURRENT_TIMESTAMP` |
153+
| Enforce uniqueness | `UNIQUE` constraint — not application-level checks |
154+
| Enforce referential integrity | `FOREIGN KEY` — not application-level checks |
155+
| Enforce value ranges | `CHECK (price > 0)` — not application-level validation |
156+
157+
---
158+
159+
## The Pattern
160+
161+
Across every layer, the pattern is the same:
162+
163+
```
164+
Platform team spends years solving the problem.
165+
Package author wraps it.
166+
You install the wrapper.
167+
The wrapper goes unmaintained.
168+
You debug the wrapper.
169+
```
170+
171+
Skip the wrapper. The platform ships with your app for free.
172+
173+
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.

‎examples/deep-clone.md‎

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# Deep Clone
2+
3+
**Task:** "Deep clone this object."
4+
5+
## Without Ponytail
6+
7+
```bash
8+
npm install lodash
9+
```
10+
11+
```js
12+
import { cloneDeep } from "lodash";
13+
14+
const copy = cloneDeep(original);
15+
```
16+
17+
Or the classic hack:
18+
19+
```js
20+
// fragile: loses Date, undefined, Map, Set, circular refs, functions
21+
const copy = JSON.parse(JSON.stringify(original));
22+
```
23+
24+
## With Ponytail
25+
26+
```js
27+
// ponytail: structuredClone does this
28+
const copy = structuredClone(original);
29+
```
30+
31+
**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.

‎examples/group-by.md‎

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# Group By
2+
3+
**Task:** "Group this array of objects by a key."
4+
5+
## Without Ponytail
6+
7+
```bash
8+
npm install lodash
9+
```
10+
11+
```js
12+
import { groupBy } from "lodash";
13+
14+
const byStatus = groupBy(orders, "status");
15+
// → { pending: [...], shipped: [...], delivered: [...] }
16+
```
17+
18+
Or the hand-rolled version:
19+
20+
```js
21+
const byStatus = orders.reduce((acc, order) => {
22+
(acc[order.status] ??= []).push(order);
23+
return acc;
24+
}, {});
25+
```
26+
27+
## With Ponytail
28+
29+
```js
30+
// ponytail: Object.groupBy does this
31+
const byStatus = Object.groupBy(orders, order => order.status);
32+
// → { pending: [...], shipped: [...], delivered: [...] }
33+
```
34+
35+
**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.

‎examples/infinite-scroll.md‎

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
# Infinite Scroll
2+
3+
**Task:** "Load more items when the user scrolls to the bottom."
4+
5+
## Without Ponytail
6+
7+
```bash
8+
npm install react-infinite-scroll-component
9+
```
10+
11+
```jsx
12+
import InfiniteScroll from "react-infinite-scroll-component";
13+
14+
export function Feed({ items, fetchMore, hasMore }) {
15+
return (
16+
<InfiniteScroll
17+
dataLength={items.length}
18+
next={fetchMore}
19+
hasMore={hasMore}
20+
loader={<Spinner />}
21+
endMessage={<p>No more items</p>}
22+
scrollThreshold={0.9}
23+
>
24+
{items.map(item => <Card key={item.id} item={item} />)}
25+
</InfiniteScroll>
26+
);
27+
}
28+
```
29+
30+
A dependency to watch scroll position and fire a callback.
31+
32+
## With Ponytail
33+
34+
```jsx
35+
// ponytail: IntersectionObserver does this, no scroll listener needed
36+
import { useEffect, useRef } from "react";
37+
38+
export function Feed({ items, fetchMore, hasMore }) {
39+
const sentinel = useRef(null);
40+
41+
useEffect(() => {
42+
const observer = new IntersectionObserver(([entry]) => {
43+
if (entry.isIntersecting && hasMore) fetchMore();
44+
});
45+
if (sentinel.current) observer.observe(sentinel.current);
46+
return () => observer.disconnect();
47+
}, [hasMore, fetchMore]);
48+
49+
return (
50+
<>
51+
{items.map(item => <Card key={item.id} item={item} />)}
52+
<div ref={sentinel} />
53+
</>
54+
);
55+
}
56+
```
57+
58+
**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.

‎examples/modal-dialog.md‎

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
# Modal Dialog
2+
3+
**Task:** "Add a modal dialog for the delete confirmation."
4+
5+
## Without Ponytail
6+
7+
```bash
8+
npm install @radix-ui/react-dialog
9+
# or: npm install react-modal
10+
```
11+
12+
```jsx
13+
import * as Dialog from "@radix-ui/react-dialog";
14+
import { useState } from "react";
15+
16+
export function DeleteModal({ onConfirm, onCancel }) {
17+
return (
18+
<Dialog.Root>
19+
<Dialog.Trigger asChild>
20+
<button className="btn-danger">Delete</button>
21+
</Dialog.Trigger>
22+
<Dialog.Portal>
23+
<Dialog.Overlay className="dialog-overlay" />
24+
<Dialog.Content className="dialog-content">
25+
<Dialog.Title>Confirm deletion</Dialog.Title>
26+
<Dialog.Description>This action cannot be undone.</Dialog.Description>
27+
<div className="dialog-actions">
28+
<Dialog.Close asChild>
29+
<button onClick={onCancel}>Cancel</button>
30+
</Dialog.Close>
31+
<button className="btn-danger" onClick={onConfirm}>Delete</button>
32+
</div>
33+
</Dialog.Content>
34+
</Dialog.Portal>
35+
</Dialog.Root>
36+
);
37+
}
38+
```
39+
40+
A dependency, a portal, an overlay, a root, a trigger, a content wrapper — to show a box with two buttons.
41+
42+
## With Ponytail
43+
44+
```html
45+
<!-- ponytail: browser has one, with focus trapping and backdrop built in -->
46+
<dialog id="confirm-delete">
47+
<p>This action cannot be undone.</p>
48+
<button id="cancel">Cancel</button>
49+
<button id="confirm">Delete</button>
50+
</dialog>
51+
```
52+
53+
```js
54+
const dialog = document.getElementById("confirm-delete");
55+
document.getElementById("cancel").onclick = () => dialog.close();
56+
document.getElementById("confirm").onclick = () => { onConfirm(); dialog.close(); };
57+
58+
// Open it:
59+
dialog.showModal();
60+
```
61+
62+
**1 dependency + 30 lines → 0 dependencies + 8 lines.** The native `<dialog>` 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.

0 commit comments

Comments
 (0)