@dryui/ui 2.0.1 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. package/dist/accordion/accordion-button-trigger.svelte +1 -0
  2. package/dist/button/button.svelte +2 -1
  3. package/dist/color-picker/color-picker-channel-input.svelte +0 -1
  4. package/dist/combobox/combobox-content.svelte +0 -1
  5. package/dist/file-select/file-select-root.svelte +75 -15
  6. package/dist/file-select/file-select-root.svelte.d.ts +2 -0
  7. package/dist/file-upload/file-upload-item.svelte +0 -1
  8. package/dist/internal/modal-content.svelte +4 -6
  9. package/dist/internal/picker-popover-content.svelte +0 -1
  10. package/dist/kbd/kbd.svelte +1 -1
  11. package/dist/mega-menu/mega-menu-panel.svelte +1 -1
  12. package/dist/menubar/menubar-content.svelte +0 -1
  13. package/dist/menubar/menubar-root.svelte +0 -1
  14. package/dist/multi-select-combobox/multi-select-combobox-content.svelte +0 -1
  15. package/dist/multi-select-combobox/multi-select-combobox-root-input.svelte +7 -3
  16. package/dist/navigation-menu/navigation-menu-link.svelte +0 -1
  17. package/dist/option-picker/option-picker-preview.svelte +1 -0
  18. package/dist/rich-text-editor/rich-text-editor-toolbar-button-input.svelte +0 -1
  19. package/dist/tags-input/tags-input-root.svelte +0 -1
  20. package/dist/toast/toast-root.svelte +0 -1
  21. package/dist/toolbar/toolbar-root.svelte +0 -1
  22. package/dist/tree/tree-item-label.svelte +0 -2
  23. package/dist/tree/tree-root.svelte +0 -1
  24. package/package.json +5 -5
  25. package/skills/dryui/SKILL.md +75 -53
  26. package/skills/dryui/rules/composition.md +3 -3
  27. package/skills/dryui/rules/compound-components.md +2 -2
  28. package/skills/dryui/rules/native-web-transitions.md +1 -1
  29. package/skills/dryui/rules/design.md +0 -71
@@ -19,6 +19,7 @@
19
19
  type="button"
20
20
  --dry-btn-justify="space-between"
21
21
  --dry-btn-radius="0"
22
+ --dry-btn-active-transform="none"
22
23
  aria-expanded={itemCtx.open}
23
24
  aria-controls={itemCtx.contentId}
24
25
  data-state={itemCtx.open ? 'open' : 'closed'}
@@ -179,6 +179,7 @@
179
179
  --_dry-btn-color: var(--dry-btn-color, var(--_dry-btn-on-accent));
180
180
  --_dry-btn-border: var(--dry-btn-border, transparent);
181
181
  --_dry-btn-radius: var(--dry-btn-radius, var(--dry-radius-md));
182
+ --_dry-btn-active-transform: var(--dry-btn-active-transform, scale(0.98));
182
183
  --_dry-btn-padding-x: var(--dry-btn-padding-x, var(--dry-space-4));
183
184
  --_dry-btn-padding-y: var(--dry-btn-padding-y, var(--dry-space-2_5));
184
185
  --_dry-btn-font-size: var(
@@ -236,7 +237,7 @@
236
237
  }
237
238
 
238
239
  &:active:not([data-disabled]) {
239
- transform: scale(0.98);
240
+ transform: var(--_dry-btn-active-transform);
240
241
  }
241
242
 
242
243
  &[data-disabled] {
@@ -59,7 +59,6 @@
59
59
  font-family: var(--dry-font-mono);
60
60
  color: var(--dry-color-text-strong);
61
61
  background: var(--dry-color-bg-raised);
62
- /* dryui-allow solid-border-on-raised: numeric color channel inputs use field affordance inside a raised picker. */
63
62
  border: 1px solid var(--dry-color-stroke-strong);
64
63
  border-radius: var(--dry-radius-md);
65
64
  transition:
@@ -94,7 +94,6 @@
94
94
  max-content
95
95
  );
96
96
  background: var(--dry-color-bg-overlay);
97
- /* dryui-allow solid-border-on-raised: popover listbox keeps a crisp edge for option scanning. */
98
97
  border: 1px solid var(--dry-color-stroke-weak);
99
98
  border-radius: var(--dry-radius-md);
100
99
  box-shadow: var(--dry-shadow-lg);
@@ -4,6 +4,8 @@
4
4
 
5
5
  interface Props {
6
6
  value?: string | null;
7
+ accept?: string | undefined;
8
+ directory?: boolean | undefined;
7
9
  onrequest?: (() => Promise<string | null>) | undefined;
8
10
  onchange?: ((value: string | null) => void) | undefined;
9
11
  disabled?: boolean | undefined;
@@ -12,6 +14,8 @@
12
14
 
13
15
  let {
14
16
  value = $bindable(null),
17
+ accept = '',
18
+ directory = true,
15
19
  onrequest,
16
20
  onchange,
17
21
  disabled = false,
@@ -19,20 +23,56 @@
19
23
  }: Props = $props();
20
24
 
21
25
  let loading = $state(false);
26
+ let fallbackInputEl: HTMLInputElement | null = null;
27
+
28
+ function commitValue(nextValue: string | null) {
29
+ if (nextValue === null) return;
30
+
31
+ value = nextValue;
32
+ onchange?.(nextValue);
33
+ }
34
+
35
+ function getFallbackInputValue(files: FileList | null): string | null {
36
+ const file = files?.[0];
37
+ if (!file) return null;
38
+
39
+ const directoryName = file.webkitRelativePath.split('/').find(Boolean);
40
+ return directory && directoryName ? directoryName : file.name;
41
+ }
42
+
43
+ function openFallbackInput() {
44
+ if (!fallbackInputEl) return;
45
+
46
+ fallbackInputEl.value = '';
47
+ fallbackInputEl.click();
48
+ }
49
+
50
+ function attachFallbackInput(node: HTMLInputElement) {
51
+ fallbackInputEl = node;
52
+ return () => {
53
+ if (fallbackInputEl === node) {
54
+ fallbackInputEl = null;
55
+ }
56
+ };
57
+ }
22
58
 
23
59
  async function defaultRequest(): Promise<string | null> {
24
60
  if (typeof window === 'undefined') return null;
25
- if (!('showDirectoryPicker' in window)) return null;
26
- try {
27
- const handle = await (
28
- window as unknown as {
29
- showDirectoryPicker: (opts: { mode: string }) => Promise<{ name: string }>;
30
- }
31
- ).showDirectoryPicker({ mode: 'read' });
32
- return handle.name;
33
- } catch {
34
- return null;
61
+ if (directory && 'showDirectoryPicker' in window) {
62
+ try {
63
+ const handle = await (
64
+ window as unknown as {
65
+ showDirectoryPicker: (opts: { mode: string }) => Promise<{ name: string }>;
66
+ }
67
+ ).showDirectoryPicker({ mode: 'read' });
68
+ return handle.name;
69
+ } catch (error) {
70
+ if (error instanceof DOMException && error.name === 'AbortError') return null;
71
+ }
35
72
  }
73
+
74
+ openFallbackInput();
75
+ return null;
36
76
  }
37
77
 
38
78
  async function request() {
@@ -41,15 +81,17 @@
41
81
  try {
42
82
  const pick = onrequest ?? defaultRequest;
43
83
  const result = await pick();
44
- if (result !== null) {
45
- value = result;
46
- onchange?.(result);
47
- }
84
+ commitValue(result);
48
85
  } finally {
49
86
  loading = false;
50
87
  }
51
88
  }
52
89
 
90
+ function handleFallbackInputChange(event: Event & { currentTarget: HTMLInputElement }) {
91
+ commitValue(getFallbackInputValue(event.currentTarget.files));
92
+ event.currentTarget.value = '';
93
+ }
94
+
53
95
  function clear() {
54
96
  if (disabled) return;
55
97
  value = null;
@@ -72,10 +114,29 @@
72
114
  </script>
73
115
 
74
116
  <div data-file-select data-disabled={disabled || undefined}>
117
+ <input
118
+ {@attach attachFallbackInput}
119
+ type="file"
120
+ {accept}
121
+ webkitdirectory={directory}
122
+ disabled={disabled || undefined}
123
+ aria-hidden="true"
124
+ tabindex="-1"
125
+ class="file-select-hidden-input"
126
+ onchange={handleFallbackInputChange}
127
+ />
128
+
75
129
  {@render children()}
76
130
  </div>
77
131
 
78
132
  <style>
133
+ .file-select-hidden-input {
134
+ display: none;
135
+ position: absolute;
136
+ height: 0;
137
+ overflow: hidden;
138
+ }
139
+
79
140
  [data-file-select] {
80
141
  container-type: inline-size;
81
142
  display: grid;
@@ -85,7 +146,6 @@
85
146
  padding: var(--dry-space-2) var(--dry-space-3);
86
147
  font-family: var(--dry-font-sans);
87
148
  background: var(--dry-color-bg-raised);
88
- /* dryui-allow solid-border-on-raised: file select is a form control and needs a persistent field edge. */
89
149
  border: 1px solid var(--dry-color-stroke-strong);
90
150
  border-radius: var(--dry-radius-md);
91
151
  transition:
@@ -1,6 +1,8 @@
1
1
  import type { Snippet } from 'svelte';
2
2
  interface Props {
3
3
  value?: string | null;
4
+ accept?: string | undefined;
5
+ directory?: boolean | undefined;
4
6
  onrequest?: (() => Promise<string | null>) | undefined;
5
7
  onchange?: ((value: string | null) => void) | undefined;
6
8
  disabled?: boolean | undefined;
@@ -27,7 +27,6 @@
27
27
  font-family: var(--dry-font-sans);
28
28
  color: var(--dry-color-text-strong);
29
29
  background: var(--dry-color-bg-raised);
30
- /* dryui-allow solid-border-on-raised: uploaded file rows need a visible item boundary in dense lists. */
31
30
  border: 1px solid var(--dry-color-stroke-weak);
32
31
  border-radius: var(--dry-radius-md);
33
32
  }
@@ -106,9 +106,9 @@
106
106
  /* ---------- Dialog (center) ---------- */
107
107
 
108
108
  [data-modal-content][data-variant='dialog'] {
109
- /* dryui-allow width */
109
+ /* dryui-allow width: resets native <dialog> sizing so the outer grid track owns the centered panel width. */
110
110
  width: 100vw;
111
- /* dryui-allow width */
111
+ /* dryui-allow width: paired with the width reset above. */
112
112
  max-width: none;
113
113
  display: grid;
114
114
  grid-template-columns: min(90vw, var(--dry-dialog-max-width, 32rem));
@@ -172,9 +172,9 @@
172
172
  /* ---------- AlertDialog (center) ---------- */
173
173
 
174
174
  [data-modal-content][data-variant='alert-dialog'] {
175
- /* dryui-allow width */
175
+ /* dryui-allow width: resets native <dialog> sizing so the outer grid track owns the centered panel width. */
176
176
  width: 100vw;
177
- /* dryui-allow width */
177
+ /* dryui-allow width: paired with the width reset above. */
178
178
  max-width: none;
179
179
  display: grid;
180
180
  grid-template-columns: min(90vw, var(--dry-dialog-max-width, 32rem));
@@ -319,7 +319,6 @@
319
319
 
320
320
  [data-modal-content][data-variant='drawer'][data-side='top'] [data-modal-panel] {
321
321
  --_drawer-rest-transform: translateY(0);
322
- /* dryui-allow symmetric-exit-animation: this is the off-canvas enter position for a top drawer, not the exit animation. */
323
322
  --_drawer-enter-transform: translateY(-100%);
324
323
  grid-row: 1;
325
324
  height: var(--dry-drawer-size);
@@ -328,7 +327,6 @@
328
327
 
329
328
  [data-modal-content][data-variant='drawer'][data-side='bottom'] [data-modal-panel] {
330
329
  --_drawer-rest-transform: translateY(0);
331
- /* dryui-allow symmetric-exit-animation: this is the off-canvas enter position for a bottom drawer, not the exit animation. */
332
330
  --_drawer-enter-transform: translateY(100%);
333
331
  grid-row: 2;
334
332
  height: var(--dry-drawer-size);
@@ -82,7 +82,6 @@
82
82
  margin: 0;
83
83
  display: inline-grid;
84
84
  padding: var(--dry-space-3);
85
- /* dryui-allow solid-border-on-raised: date picker popovers need a clear calendar boundary. */
86
85
  border: 1px solid var(--dry-color-stroke-weak);
87
86
  border-radius: var(--dry-radius-lg);
88
87
  background: var(--dry-color-bg-overlay);
@@ -45,7 +45,7 @@
45
45
  font-size: var(--dry-kbd-font-size);
46
46
  font-weight: 600;
47
47
  line-height: 1;
48
- box-shadow: inset 0 -1px 0 color-mix(in srgb, var(--dry-color-text-strong) 10%, transparent);
48
+ box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--dry-color-text-strong) 10%, transparent);
49
49
  user-select: none;
50
50
  white-space: nowrap;
51
51
  }
@@ -117,7 +117,7 @@
117
117
  border-radius: var(--dry-radius-lg, 0.5rem);
118
118
  box-shadow: var(--dry-shadow-lg, 0 10px 15px -3px rgba(0, 0, 0, 0.1));
119
119
  padding: var(--dry-space-4, 1rem);
120
- /* dryui-allow width */
120
+ /* dryui-allow width: viewport cap for the anchored floating panel, not a child-layout sizing rule. */
121
121
  max-inline-size: var(--dry-mega-menu-panel-max-width, min(92vw, 60rem));
122
122
  display: grid;
123
123
  grid-auto-flow: column;
@@ -123,7 +123,6 @@
123
123
 
124
124
  background: var(--dry-color-bg-overlay);
125
125
  color: var(--dry-color-text-strong);
126
- /* dryui-allow solid-border-on-raised: menu popover keeps a visible edge while floating above varied content. */
127
126
  border: 1px solid var(--dry-color-stroke-weak);
128
127
  border-radius: var(--dry-radius-popover);
129
128
  box-shadow: var(--dry-shadow-overlay);
@@ -82,7 +82,6 @@
82
82
  gap: var(--dry-space-1);
83
83
  padding: var(--dry-space-1);
84
84
  background: var(--dry-color-bg-overlay);
85
- /* dryui-allow solid-border-on-raised: menubar root presents a persistent command strip boundary. */
86
85
  border: 1px solid var(--dry-color-stroke-weak);
87
86
  border-radius: var(--dry-radius-lg);
88
87
  }
@@ -87,7 +87,6 @@
87
87
  margin: 0;
88
88
  grid-template-columns: minmax(anchor-size(inline), max-content);
89
89
  background: var(--dry-color-bg-overlay);
90
- /* dryui-allow solid-border-on-raised: popover menu keeps an explicit edge for contrast against busy app surfaces. */
91
90
  border: 1px solid var(--dry-color-stroke-weak);
92
91
  border-radius: var(--dry-radius-md);
93
92
  box-shadow: var(--dry-shadow-lg);
@@ -82,11 +82,16 @@
82
82
  }
83
83
 
84
84
  function selectValue(itemValue: string) {
85
- if (disabled || value.includes(itemValue) || !canSelectValue(itemValue)) {
85
+ if (disabled || !canSelectValue(itemValue)) {
86
86
  return false;
87
87
  }
88
88
 
89
- setSelectedValues([...value, itemValue]);
89
+ if (value.includes(itemValue)) {
90
+ setSelectedValues(value.filter((entry) => entry !== itemValue));
91
+ } else {
92
+ setSelectedValues([...value, itemValue]);
93
+ }
94
+
90
95
  setQueryValue('');
91
96
  activeItemId = '';
92
97
  focusInput();
@@ -251,7 +256,6 @@
251
256
  justify-content: start;
252
257
  gap: var(--dry-space-1_5);
253
258
  padding: var(--dry-space-2);
254
- /* dryui-allow solid-border-on-raised: composite input needs a persistent field edge plus raised fill for token contrast. */
255
259
  border: 1px solid var(--dry-color-stroke-strong);
256
260
  border-radius: var(--dry-radius-md);
257
261
  background: var(--dry-color-bg-raised);
@@ -39,7 +39,6 @@
39
39
  [data-nav-menu-link][data-active] {
40
40
  background: var(--dry-color-fill-brand-weak);
41
41
  color: var(--dry-color-text-brand);
42
- box-shadow: inset 2px 0 0 var(--dry-color-stroke-selected);
43
42
  font-weight: 600;
44
43
  }
45
44
 
@@ -87,6 +87,7 @@
87
87
  var(--_preset-color) 58%,
88
88
  color-mix(in srgb, black 12%, var(--_preset-color) 88%) 100%
89
89
  );
90
+ /* dryui-allow inset-shadow: 1px top highlight + 1px bottom shadow give the preset color swatch a beveled material look. */
90
91
  box-shadow:
91
92
  inset 0 1px 0 color-mix(in srgb, white 22%, transparent),
92
93
  inset 0 -1px 0 color-mix(in srgb, black 10%, transparent);
@@ -516,7 +516,6 @@
516
516
  gap: var(--dry-space-2);
517
517
  padding: var(--dry-space-2);
518
518
  background: var(--dry-color-bg-overlay);
519
- /* dryui-allow solid-border-on-raised: inline link editor is a small popover that needs a distinct edge. */
520
519
  border: 1px solid var(--dry-rte-border);
521
520
  border-radius: var(--dry-radius-md);
522
521
  box-shadow: var(--dry-shadow-md);
@@ -86,7 +86,6 @@
86
86
  [data-part='root'] {
87
87
  display: block;
88
88
  padding: var(--dry-space-2);
89
- /* dryui-allow solid-border-on-raised: tag editor is a form field with token chips on a raised input surface. */
90
89
  border: 1px solid var(--dry-color-stroke-strong);
91
90
  border-radius: var(--dry-radius-md);
92
91
  background: var(--dry-color-bg-raised);
@@ -108,7 +108,6 @@
108
108
  }
109
109
  }
110
110
 
111
- /* dryui-allow ad-hoc-enter-keyframe: toast predates motion wrappers and keeps reduced-motion handling in this file. */
112
111
  @keyframes slideIn {
113
112
  from {
114
113
  opacity: 0;
@@ -89,7 +89,6 @@
89
89
  gap: var(--dry-space-1);
90
90
  padding: var(--dry-space-1);
91
91
  background: var(--dry-color-bg-overlay);
92
- /* dryui-allow solid-border-on-raised: toolbar chrome needs a visible control group boundary. */
93
92
  border: 1px solid var(--dry-color-stroke-weak);
94
93
  border-radius: var(--dry-radius-lg);
95
94
  }
@@ -51,8 +51,6 @@
51
51
  [data-part='label'][data-selected] {
52
52
  background: var(--dry-tree-item-selected-bg, var(--dry-color-fill-brand-weak));
53
53
  color: var(--dry-tree-item-selected-color, var(--dry-color-text-brand));
54
- box-shadow: inset 2px 0 0
55
- var(--dry-tree-item-selected-indicator, var(--dry-color-stroke-selected));
56
54
  font-weight: 600;
57
55
  }
58
56
  </style>
@@ -238,7 +238,6 @@
238
238
  --dry-tree-item-hover-bg: var(--dry-color-fill);
239
239
  --dry-tree-item-selected-bg: var(--dry-color-fill-brand-weak);
240
240
  --dry-tree-item-selected-color: var(--dry-color-text-brand);
241
- --dry-tree-item-selected-indicator: var(--dry-color-stroke-selected);
242
241
  --dry-tree-icon-size: 1rem;
243
242
  }
244
243
  </style>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dryui/ui",
3
- "version": "2.0.1",
3
+ "version": "3.0.0",
4
4
  "description": "Zero-dependency styled Svelte 5 components with scoped styles and --dry-* CSS variable theming.",
5
5
  "author": "Rob Balfre",
6
6
  "license": "MIT",
@@ -817,14 +817,14 @@
817
817
  "check:publish-hygiene": "bun run check:publint && bun run check:attw"
818
818
  },
819
819
  "dependencies": {
820
- "@dryui/primitives": "^2.0.1"
820
+ "@dryui/primitives": "^3.0.0"
821
821
  },
822
822
  "peerDependencies": {
823
- "svelte": "^5.55.4"
823
+ "svelte": "^5.55.5"
824
824
  },
825
825
  "devDependencies": {
826
- "@dryui/lint": "^0.7.0",
827
- "svelte": "^5.55.4",
826
+ "@dryui/lint": "^1.0.0",
827
+ "svelte": "^5.55.5",
828
828
  "@sveltejs/package": "^2.5.7",
829
829
  "svelte-check": "^4.4.6",
830
830
  "typescript": "^6.0.3"
@@ -9,22 +9,47 @@ Zero-dependency Svelte 5 components. All imports from `@dryui/ui`. Requires a th
9
9
 
10
10
  **Tradeoff:** These rules bias toward correctness over speed. For throwaway prototypes, use judgment.
11
11
 
12
+ ## UI Creation Pipeline
13
+
14
+ DryUI work is explicit. Confirm contracts, build, then validate.
15
+
16
+ 1. **User brief** — one line capturing what you are building and for whom.
17
+ 2. **DryUI lookup/plan** — use `dryui ask` or MCP `ask` to confirm component contracts, tokens, recipes, and accessibility notes before choosing components.
18
+ 3. **Implementation** — build with DryUI components, Svelte 5 runes, grid layout, `--dry-*` tokens, and accessible composition.
19
+ 4. **Deterministic check** — run `dryui check [path]` or MCP `check` to catch contract drift, accessibility regressions, token drift, and CSS discipline violations.
20
+
21
+ ## Design guidance, critique, polish
22
+
23
+ DryUI is zero-dependency components + tokens + contracts. It deliberately does NOT ship design opinion. For design-quality flows like brief, critique, polish, visual review, or anti-pattern detection, use [impeccable](https://impeccable.style), which is installed alongside DryUI by `dryui init` or via `npx impeccable skills install`.
24
+
25
+ Invoke from your AI harness:
26
+
27
+ - `/impeccable teach` — one-time: scaffold `PRODUCT.md` + `DESIGN.md`
28
+ - `/impeccable craft` — design-then-build a feature
29
+ - `/impeccable shape` — plan UX/UI before writing code
30
+ - `/impeccable critique <target>` — UX design review
31
+ - `/impeccable audit <target>` — a11y, performance, responsive checks
32
+ - `/impeccable polish <target>` — final pass before shipping
33
+ - Full catalog: https://impeccable.style/cheatsheet
34
+
35
+ `PRODUCT.md` and `DESIGN.md` at the project root are impeccable-owned. DryUI tools do not read or write them. Anti-pattern detection: `npx impeccable detect <path-or-url>`.
36
+
12
37
  ## 1. Look Up Before You Write
13
38
 
14
39
  **Never guess a component API. Always verify first.**
15
40
 
16
- - Call `dryui info <component>` or `dryui compose "<query>"` before using any component for the first time. If MCP is available, `ask --scope component` and `ask --scope recipe` are equivalent.
17
- - Component APIs vary `bind:value`, `bind:open`, `bind:checked` are NOT interchangeable
18
- - Compound vs simple, required parts, available props — all differ per component
19
- - If you skip the lookup, you'll write plausible-looking code that silently breaks
41
+ - Call `dryui ask --scope component "<Component>"` or `dryui ask --scope recipe "<pattern>"` before using any component for the first time. MCP `ask` is the equivalent surface.
42
+ - Component APIs vary. `bind:value`, `bind:open`, `bind:checked` are NOT interchangeable.
43
+ - Compound vs simple, required parts, available props — all differ per component.
44
+ - If you skip the lookup, you'll write plausible-looking code that silently breaks.
20
45
 
21
- The test: can you point to a `dryui info`, `dryui compose`, or `ask` call for every component or pattern in your output?
46
+ The test: can you point to a `dryui ask` or MCP `ask` call for every component or pattern in your output?
22
47
 
23
48
  ## 2. Everything is Compound Until Proven Otherwise
24
49
 
25
50
  **Use `.Root`. Always check.**
26
51
 
27
- Most DryUI components are compound they require `<Card.Root>`, not `<Card>`. The bare name silently fails or renders wrong. Assume compound; verify with `dryui info <Component>` or `ask --scope component`.
52
+ Most DryUI components are compound. They require `<Card.Root>`, not `<Card>`. The bare name silently fails or renders wrong. Assume compound, verify with `ask --scope component`.
28
53
 
29
54
  ```svelte
30
55
  <!-- Wrong -->
@@ -33,7 +58,7 @@ Most DryUI components are compound — they require `<Card.Root>`, not `<Card>`.
33
58
  <Card.Root>content</Card.Root>
34
59
  ```
35
60
 
36
- Compound components are tracked in the manifest at `packages/mcp/src/component-catalog.ts`. Verify with `dryui info <Component>` or `ask --scope component` before you assume a bare name works, then use `.Root` and wrap the parts inside it.
61
+ Compound components are tracked in the manifest at `packages/mcp/src/component-catalog.ts`. Verify with `ask --scope component` before you assume a bare name works, then use `.Root` and wrap the parts inside it.
37
62
 
38
63
  The test: every compound component in your markup uses `.Root`, and its parts are wrapped inside it. See `rules/compound-components.md` for the parts reference.
39
64
 
@@ -41,11 +66,11 @@ The test: every compound component in your markup uses `.Root`, and its parts ar
41
66
 
42
67
  **Import it. Use its tokens. Don't fight it.**
43
68
 
44
- - Import `@dryui/ui/themes/default.css` (and `dark.css`) before any component use
45
- - Use `--dry-color-*` and `--dry-space-*` tokens never hardcoded colors or spacing
46
- - Don't add decorative CSS (gradients, shadows, colored borders) the theme handles appearance
47
- - Override semantic tokens (Tier 2) in `:root`, not component tokens (Tier 3)
48
- - Prefer `<html class="theme-auto">` use `data-theme="light|dark"` only for explicit overrides
69
+ - Import `@dryui/ui/themes/default.css` (and `dark.css`) before any component use.
70
+ - Use `--dry-color-*` and `--dry-space-*` tokens. Never hardcode colors or spacing.
71
+ - Don't add decorative CSS (gradients, shadows, colored borders). The theme handles appearance.
72
+ - Override semantic tokens (Tier 2) in `:root`, not component tokens (Tier 3).
73
+ - Prefer `<html class="theme-auto">`. Use `data-theme="light|dark"` only for explicit overrides.
49
74
 
50
75
  ```css
51
76
  /* Wrong */
@@ -63,14 +88,16 @@ The test: every compound component in your markup uses `.Root`, and its parts ar
63
88
 
64
89
  The test: does your CSS contain zero hex colors, zero `rgb()` values, and zero inline styles?
65
90
 
91
+ Theming precedence beats design opinion. If impeccable guidance conflicts with DryUI theme contracts, tokens, or accessibility rules, DryUI wins.
92
+
66
93
  ## 4. Grid for Layout. Container for Width. @container for Responsive.
67
94
 
68
95
  **Nothing else.**
69
96
 
70
- - All layout is `display: grid` with `--dry-space-*` tokens in scoped `<style>`
71
- - `Container` (simple component, no `.Root`) for constrained content width
72
- - Use `@container` queries for responsive sizing never `@media` for layout breakpoints
73
- - No flexbox. No inline styles. No `width`/`min-width`/`max-width` properties
97
+ - All layout is `display: grid` with `--dry-space-*` tokens in scoped `<style>`.
98
+ - `Container` (simple component, no `.Root`) for constrained content width.
99
+ - Use `@container` queries for responsive sizing. Never `@media` for layout breakpoints.
100
+ - No flexbox. No inline styles. No `width`/`min-width`/`max-width` properties.
74
101
 
75
102
  ```svelte
76
103
  <div class="layout">...</div>
@@ -83,29 +110,29 @@ The test: does your CSS contain zero hex colors, zero `rgb()` values, and zero i
83
110
  </style>
84
111
  ```
85
112
 
86
- The test: grep your output for `display: flex`, `style=`, `@media` all should return nothing.
113
+ The test: grep your output for `display: flex`, `style=`, `@media`. All should return nothing.
87
114
 
88
115
  ## 4A. Escape Hatches Mean Stop.
89
116
 
90
117
  **If lint or the compiler pushes you toward an escape hatch, the structure is usually wrong.**
91
118
 
92
- - Never add `:global()`, `!important`, `all: unset`, `<svelte:element>`, or `<!-- svelte-ignore ... -->` just to make a selector or warning go away
93
- - Never add `width`, `min-width`, `max-width`, `inline-size`, `min-inline-size`, or `max-inline-size` to solve layout pressure
94
- - Never use raw native elements outside their canonical DryUI component directories just because composition feels inconvenient
95
- - Never pass `class=` to DryUI components expecting it to style their internals; use wrapper elements, component props, `data-*` attributes, or `--dry-*` tokens instead
96
- - When blocked, restructure the markup instead: add a local wrapper, split explicit `{#if}` branches, move sizing to parent grid tracks, or promote the pattern into the canonical component where the raw element belongs
97
- - Treat `dryui/no-global`, `dryui/no-important`, `dryui/no-width`, `dryui/no-raw-native-element`, `dryui/no-css-ignore`, and `dryui/no-svelte-element` as design feedback, not obstacles to suppress
119
+ - Never add `:global()`, `!important`, `all: unset`, `<svelte:element>`, or `<!-- svelte-ignore ... -->` just to make a selector or warning go away.
120
+ - Never add `width`, `min-width`, `max-width`, `inline-size`, `min-inline-size`, or `max-inline-size` to solve layout pressure.
121
+ - Never use raw native elements outside their canonical DryUI component directories just because composition feels inconvenient.
122
+ - Never pass `class=` to DryUI components expecting it to style their internals. Use wrapper elements, component props, `data-*` attributes, or `--dry-*` tokens instead.
123
+ - When blocked, restructure the markup instead: add a local wrapper, split explicit `{#if}` branches, move sizing to parent grid tracks, or promote the pattern into the canonical component where the raw element belongs.
124
+ - Treat `dryui/no-global`, `dryui/no-important`, `dryui/no-width`, `dryui/no-raw-native-element`, `dryui/no-css-ignore`, and `dryui/no-svelte-element` as design feedback, not obstacles to suppress.
98
125
 
99
- The test: grep your output for `:global(`, `!important`, `all: unset`, `svelte-ignore`, `svelte:element`, raw `<button`, raw `<input`, raw `<select`, raw `<dialog`, raw `<hr`, raw `<table`, and `width:` all should return nothing unless you are editing the canonical component that owns that native element.
126
+ The test: grep your output for `:global(`, `!important`, `all: unset`, `svelte-ignore`, `svelte:element`, raw `<button`, raw `<input`, raw `<select`, raw `<dialog`, raw `<hr`, raw `<table`, and `width:`. All should return nothing unless you are editing the canonical component that owns that native element.
100
127
 
101
128
  ## 5. Every Input Gets a Field.Root
102
129
 
103
130
  **Accessibility isn't optional.**
104
131
 
105
- - Wrap every form input in `Field.Root` with a `Label`
106
- - Use `AlertDialog` (not `Dialog`) for destructive confirmations
107
- - Add `aria-label` to every icon-only button
108
- - Use `type="submit"` on primary form action buttons
132
+ - Wrap every form input in `Field.Root` with a `Label`.
133
+ - Use `AlertDialog` (not `Dialog`) for destructive confirmations.
134
+ - Add `aria-label` to every icon-only button.
135
+ - Use `type="submit"` on primary form action buttons.
109
136
 
110
137
  ```svelte
111
138
  <!-- Wrong -->
@@ -125,9 +152,9 @@ The test: every `<Input>`, `<Select.Root>`, `<Textarea>` is inside a `Field.Root
125
152
 
126
153
  **If a DryUI component exists for it, use it.**
127
154
 
128
- `DatePicker` not `<input type="date">`. `Select.Root` not `<select>`. `Dialog.Root` not `<dialog>`. `Separator` not `<hr>`. `Button` not `<button>`. DryUI components handle theming and accessibility automatically native elements don't.
155
+ `DatePicker` not `<input type="date">`. `Select.Root` not `<select>`. `Dialog.Root` not `<dialog>`. `Separator` not `<hr>`. `Button` not `<button>`. DryUI components handle theming and accessibility automatically. Native elements don't.
129
156
 
130
- The test: search your markup for raw `<input`, `<select>`, `<dialog>`, `<button>`, `<hr>`, `<table>` each should be a DryUI component instead.
157
+ The test: search your markup for raw `<input`, `<select>`, `<dialog>`, `<button>`, `<hr>`, `<table>`. Each should be a DryUI component instead.
131
158
 
132
159
  ## 7. Ask the Svelte MCP for Svelte Questions
133
160
 
@@ -135,7 +162,7 @@ The test: search your markup for raw `<input`, `<select>`, `<dialog>`, `<button>
135
162
 
136
163
  For Svelte 5 runes (`$state`, `$derived`, `$effect`, `$props`), snippets, SvelteKit load fns, `+page.server.ts` shape, form actions, and anything Svelte-syntax adjacent: call the official `svelte-autofixer` and `get-documentation` tools from `@sveltejs/mcp` before guessing from memory.
137
164
 
138
- - `dryui setup --install` registers `@sveltejs/mcp` by default; pass `--no-svelte-mcp` to skip.
165
+ - `dryui setup --install` registers `@sveltejs/mcp` by default. Pass `--no-svelte-mcp` to skip.
139
166
  - If it's not registered, the fallback is the remote endpoint `https://mcp.svelte.dev/mcp` or a one-liner like `claude mcp add -t stdio -s user svelte -- npx -y @sveltejs/mcp`.
140
167
  - Scope split: DryUI `ask`/`check` cover component APIs, theming, composition, and validation. Svelte MCP covers the runtime, compiler, and framework idioms.
141
168
 
@@ -159,13 +186,13 @@ dryui
159
186
 
160
187
  ```bash
161
188
  dryui init # existing project
162
- dryui init my-app # new project scaffolds SvelteKit + DryUI in one step
189
+ dryui init my-app # new project, scaffolds SvelteKit + DryUI in one step
163
190
  cd my-app && bun run dev
164
191
  ```
165
192
 
166
193
  This works for greenfield (empty directory), brownfield (existing non-SvelteKit project), and existing SvelteKit projects. On existing projects, `dryui install` prints the ordered plan and `dryui detect` verifies that setup is complete.
167
194
 
168
- > **No global install?** `bunx @dryui/cli <cmd>` and `npx -y @dryui/cli <cmd>` work anywhere without installing same commands, just slower (re-fetches on each call).
195
+ > **No global install?** `bunx @dryui/cli <cmd>` and `npx -y @dryui/cli <cmd>` work anywhere without installing. Same commands, just slower (re-fetches on each call).
169
196
 
170
197
  **4. Add the editor integration layer** after the CLI is working:
171
198
 
@@ -174,12 +201,12 @@ This works for greenfield (empty directory), brownfield (existing non-SvelteKit
174
201
  - OpenCode: `npx degit rob-balfre/dryui/packages/ui/skills/dryui .opencode/skills/dryui` + add the `dryui` and `dryui-feedback` local MCP servers in `opencode.json` (OpenCode also loads `.agents/skills/dryui` and reads `AGENTS.md`)
175
202
  - Copilot/Cursor/Windsurf: `npx degit rob-balfre/dryui/packages/ui/skills/dryui .agents/skills/dryui` + add MCP config (see https://dryui.dev/tools)
176
203
 
177
- **5. Register the Svelte MCP companion.** `dryui setup --install` does this automatically for Copilot, Cursor, OpenCode, Windsurf, and Zed. For Claude Code run `claude mcp add -t stdio -s user svelte -- npx -y @sveltejs/mcp`; for Codex add `[mcp_servers.svelte] command = "npx", args = ["-y", "@sveltejs/mcp"]` to `~/.codex/config.toml`. See rule 7 above.
204
+ **5. Register the Svelte MCP companion.** `dryui setup --install` does this automatically for Copilot, Cursor, OpenCode, Windsurf, and Zed. For Claude Code run `claude mcp add -t stdio -s user svelte -- npx -y @sveltejs/mcp`. For Codex add `[mcp_servers.svelte] command = "npx", args = ["-y", "@sveltejs/mcp"]` to `~/.codex/config.toml`. See rule 7 above.
178
205
 
179
206
  ### Manual setup
180
207
 
181
208
  1. `bun add @dryui/ui`
182
- 2. `bun add -d @dryui/lint` enforces grid-only layout, bans flexbox/inline-style/width at build time. Without this step the CSS discipline rules are not enforced at build time, and only post-write `check` / CLI validation remain.
209
+ 2. `bun add -d @dryui/lint`. Enforces grid-only layout, bans flexbox/inline-style/width at build time. Without this step the CSS discipline rules are not enforced at build time, and only post-write `check` / CLI validation remain.
183
210
  3. Wire the lint preprocessor in `svelte.config.js` (add `dryuiLint` as the **first** item in the `preprocess` array):
184
211
 
185
212
  ```js
@@ -199,7 +226,7 @@ This works for greenfield (empty directory), brownfield (existing non-SvelteKit
199
226
  export default config;
200
227
  ```
201
228
 
202
- 4. Add `class="theme-auto"` to `<html>` in `src/app.html`
229
+ 4. Add `class="theme-auto"` to `<html>` in `src/app.html`.
203
230
  5. In root layout (`src/routes/+layout.svelte`), import themes:
204
231
  ```svelte
205
232
  <script>
@@ -207,18 +234,18 @@ This works for greenfield (empty directory), brownfield (existing non-SvelteKit
207
234
  import '@dryui/ui/themes/dark.css';
208
235
  </script>
209
236
  ```
210
- 6. Import `app.css` AFTER theme CSS if you have custom styles
237
+ 6. Import `app.css` AFTER theme CSS if you have custom styles.
211
238
 
212
- > `dryui init` applies all six steps automatically prefer it over manual setup when you can.
239
+ > `dryui init` applies all six steps automatically. Prefer it over manual setup when you can.
213
240
 
214
- ## Bindable Props Common Confusion
241
+ ## Bindable Props, Common Confusion
215
242
 
216
- Always verify with `dryui info <Component>` or `ask --scope component`, but these are the most common mistakes:
243
+ Always verify with `ask --scope component`, but these are the most common mistakes:
217
244
 
218
245
  - `bind:value` (Input, Select, Tabs...) vs `bind:checked` (Checkbox, Switch) vs `bind:pressed` (Toggle) vs `bind:open` (Dialog, Popover, Drawer...)
219
- - Select and Combobox support both `bind:value` and `bind:open`
220
- - ColorPicker also exposes `bind:alpha`; Transfer uses `bind:sourceItems` / `bind:targetItems`
221
- - Tour uses `bind:active`, not `bind:open`
246
+ - Select and Combobox support both `bind:value` and `bind:open`.
247
+ - ColorPicker also exposes `bind:alpha`. Transfer uses `bind:sourceItems` / `bind:targetItems`.
248
+ - Tour uses `bind:active`, not `bind:open`.
222
249
 
223
250
  ## Tools
224
251
 
@@ -226,9 +253,9 @@ Use these to look up APIs, discover components, plan setup, and validate code.
226
253
 
227
254
  ### Recommended workflow
228
255
 
229
- 1. `dryui info <Component>` or `dryui compose "<query>"` before writing so you confirm kind, required parts, bindables, and canonical usage. If MCP is available, `ask --scope component` and `ask --scope recipe` are the equivalent surface.
256
+ 1. Resolve any component or recipe uncertainty with `dryui ask --scope component "<Component>"` or `dryui ask --scope recipe "<pattern>"`. If MCP is available, `ask --scope component` and `ask --scope recipe` are the equivalent surface.
230
257
  2. Build the route or component with raw CSS grid, `Container` for constrained width, and `@container` for responsive layout.
231
- 3. Run `dryui check [path]` or MCP `check` after implementation to catch composition drift, layout violations, and accessibility regressions. Use `dryui check --visual <url>` or MCP `check` with `visualUrl` when rendered pixels need review.
258
+ 3. Run `dryui check [path]` or MCP `check` after implementation to catch composition drift, layout violations, accessibility regressions, and token drift.
232
259
  4. Never guess component shape from memory. DryUI is intentionally strict, and the lookup cost is lower than rework.
233
260
 
234
261
  ### CLI (default entry point)
@@ -239,12 +266,10 @@ Install once with `bun install -g @dryui/cli@latest` (or `npm install -g @dryui/
239
266
  dryui # default onboarding entry point
240
267
  dryui setup # explicit onboarding subcommand
241
268
  dryui init [path] [--pm bun] # Bootstrap SvelteKit + DryUI project
242
- dryui info <component> # Look up component API
243
- dryui compose "date input" # Composition guidance
269
+ dryui ask <scope> "<query>" # Look up components, recipes, tokens, setup
244
270
  dryui detect [path] # Check project setup
245
271
  dryui install [path] # Print install plan
246
272
  dryui check [path] # Validate file, theme, directory, or workspace
247
- dryui check --visual <url> # Screenshot a URL and critique rendered polish
248
273
  dryui list # List components
249
274
  dryui tokens --category color # Browse design tokens
250
275
  dryui ambient # SessionStart context
@@ -252,7 +277,7 @@ dryui install-hook --dry-run # Preview Claude hook wiring
252
277
  dryui feedback init # Feedback tooling setup
253
278
  ```
254
279
 
255
- Without a global install, prefix any command with `bunx @dryui/cli …` or `npx -y @dryui/cli …` same behaviour, just slower (re-fetches on each call).
280
+ Without a global install, prefix any command with `bunx @dryui/cli …` or `npx -y @dryui/cli …`. Same behaviour, just slower (re-fetches on each call).
256
281
 
257
282
  ### MCP tools (same workflow in-editor)
258
283
 
@@ -262,7 +287,6 @@ Without a global install, prefix any command with `bunx @dryui/cli …` or `npx
262
287
  | Lookup & composition | `ask --scope component`, `ask --scope recipe`, `ask --scope list` |
263
288
  | Validation | `check <file.svelte>`, `check <theme.css>` |
264
289
  | Audit | `check`, `check <directory>` |
265
- | Rendered UI | `check` with `visualUrl`, or direct `check-vision` |
266
290
 
267
291
  Categories: action, input, form, layout, navigation, overlay, display, feedback, interaction, utility
268
292
 
@@ -275,10 +299,8 @@ Read these when you need deeper guidance:
275
299
  - **`rules/composition.md`** — Form patterns, page layouts, composition recipes
276
300
  - **`rules/accessibility.md`** — Field.Root, ARIA, focus management, pre-ship checklist
277
301
  - **`rules/svelte.md`** — Runes, snippets, native browser APIs, styling rules
278
- - **`rules/design.md`** — Minimal code, no premature abstraction, naming conventions
279
- - **`rules/visual-effects-performance.md`** — Tiered budgets and implementation rules for shader, blur, glass, and pointer-reactive effects
280
302
  - **`rules/native-web-transitions.md`** — View Transition API, scroll animations, reduced-motion
281
303
 
282
304
  ---
283
305
 
284
- **These rules are working if:** every component traces to a lookup, diffs contain zero hardcoded colors, and the reviewer finds nothing.
306
+ **These rules are working if:** every component traces to a lookup, diffs contain zero hardcoded colors, and `dryui check` finds nothing.
@@ -398,7 +398,7 @@ Use Field.Error to show validation messages.
398
398
 
399
399
  ## Component Selection Quick Reference
400
400
 
401
- Before using any component, call `dryui compose "<query>"` or `dryui info <Component>` to get the correct component and usage snippet. If MCP is available, `ask --scope recipe` and `ask --scope component` are equivalent. This table is a quick reference the CLI and MCP surfaces both return the fuller snippets and anti-patterns.
401
+ Before using any component, call `dryui ask --scope recipe "<pattern>"` (for layouts) or `dryui ask --scope component "<Component>"` (for APIs) to get the correct component and usage snippet. This table is a quick reference. The CLI and MCP surfaces both return the fuller snippets and anti-patterns.
402
402
 
403
403
  | UI Need | Use This | NOT This |
404
404
  | ----------------- | -------------------------------------- | ---------------------------- |
@@ -431,7 +431,7 @@ Before using any component, call `dryui compose "<query>"` or `dryui info <Compo
431
431
 
432
432
  ## Composition Recipes
433
433
 
434
- Call `dryui compose "<recipe>"` with any recipe name to get a full working snippet. If MCP is available, `ask --scope recipe "<recipe>"` is equivalent.
434
+ Call `dryui ask --scope recipe "<recipe>"` with any recipe name to get a full working snippet.
435
435
 
436
436
  | Recipe | Description | Key Components |
437
437
  | ------------------------- | ------------------------- | -------------------------------------- |
@@ -457,7 +457,7 @@ DryUI is a presentation and accessibility system, not a workflow engine. For dep
457
457
  - Normalize route/session state in script before rendering DryUI inputs.
458
458
  - Reset dependent `Select.Root` values when their parent choice changes; do not rely on stale child state surviving domain changes.
459
459
  - Use raw CSS grid to lay out planner sections, and keep orchestration logic in route-level stores or derived state.
460
- - Run `dryui info <Component>` or `dryui compose "<pattern>"` before introducing a new field shape, then run `dryui check [path]` or MCP `check` after the flow is wired. Use `dryui check --visual <url>` or MCP `check` with `visualUrl` when the rendered page needs visual review.
460
+ - Run `dryui ask --scope component "<Component>"` or `dryui ask --scope recipe "<pattern>"` before introducing a new field shape, then run `dryui check [path]` or MCP `check` after the flow is wired.
461
461
 
462
462
  ```svelte
463
463
  <script lang="ts">
@@ -18,7 +18,7 @@ Every compound component uses `.Root` as the container. Never use the bare name.
18
18
 
19
19
  ## Parts Reference
20
20
 
21
- Below are the parts for the most commonly used compound components. Prefer `dryui info <name>` for the full, up-to-date parts list; if MCP is available, `ask --scope component "<name>"` is equivalent.
21
+ Below are the parts for the most commonly used compound components. Prefer `dryui ask --scope component "<name>"` for the full, up-to-date parts list.
22
22
 
23
23
  ### Card
24
24
 
@@ -307,6 +307,6 @@ Parts: Root, Input, Content, Item, Empty
307
307
 
308
308
  ## Full Compound Component List
309
309
 
310
- Run `dryui info <name>` for any component's complete parts list. If MCP is available, `ask --scope component "<name>"` is equivalent:
310
+ Run `dryui ask --scope component "<name>"` for any component's complete parts list:
311
311
 
312
312
  Accordion, AlertDialog, Breadcrumb, Card, Collapsible, ColorPicker, Combobox, CommandPalette, ContextMenu, DataGrid, DatePicker, Dialog, DragAndDrop, Drawer, DropdownMenu, EmptyState, Field, FileUpload, FloatButton, Pagination, Popover, RadioGroup, RichTextEditor, Select, Splitter, Stepper, Table, Tabs, TagsInput, Toast, ToggleGroup, Toolbar, Tooltip, Tour, Transfer
@@ -80,7 +80,7 @@ Always include a reduced-motion override:
80
80
  }
81
81
  ```
82
82
 
83
- The `@media` block comes after the default rule in the same scoped stylesheet, so equal specificity plus source order handles the override without `!important` — which is banned by `@dryui/lint` (`dryui/no-important`). In JS, skip delayed typewriter/replay steps and render the final state immediately when reduced motion is active.
83
+ The `@media` block comes after the default rule in the same scoped stylesheet, so equal specificity plus source order handles the override without `!important` (banned by `@dryui/lint` via `dryui/no-important`). In JS, skip delayed typewriter/replay steps and render the final state immediately when reduced motion is active.
84
84
 
85
85
  ## Svelte notes
86
86
 
@@ -1,71 +0,0 @@
1
- # Clean Code Standards
2
-
3
- ## Core Principle
4
-
5
- Write the minimum correct code. Every line must earn its place.
6
-
7
- ## Rules
8
-
9
- ### No Premature Abstraction
10
-
11
- - Three similar lines > one premature helper
12
- - Only extract when you have 3+ real call sites
13
- - No "just in case" configurability, feature flags, or extension points
14
- - Delete code you don't need -- don't comment it out
15
-
16
- ### No Noise
17
-
18
- - No comments that restate what code does
19
- - Comments only where the _why_ is non-obvious
20
- - No empty catch blocks -- handle or rethrow
21
- - No unused imports, variables, or parameters
22
- - No `console.log` left in production code
23
- - No commented-out code -- use git history
24
-
25
- ### Names Are Documentation
26
-
27
- - Functions: verb + noun (`getUser`, `handleClick`, `parseDate`)
28
- - Booleans: `is`/`has`/`should` prefix (`isOpen`, `hasError`)
29
- - Collections: plural (`users`, `items`)
30
- - Callbacks: `on` + event (`onClose`, `onChange`)
31
- - Constants: UPPER_SNAKE only for true compile-time constants
32
-
33
- ### Functions
34
-
35
- - One job per function
36
- - Max 3 parameters -- use an options object for more
37
- - Return early to avoid nesting
38
- - Pure functions where possible -- side effects at the edges
39
-
40
- ### Error Handling
41
-
42
- - Only validate at system boundaries (user input, API responses, file reads)
43
- - Trust internal code -- don't null-check values you just created
44
- - Use specific error types, not generic strings
45
- - Handle errors at the level that can meaningfully respond
46
-
47
- ### File Organization
48
-
49
- - One concept per file
50
- - Keep files under 300 lines -- split if growing
51
- - Colocate related code (component + styles + tests in same directory)
52
- - Index files only for re-exports, never for logic
53
-
54
- ## Anti-Patterns -- Stop and Fix
55
-
56
- | If you're about to... | Instead... |
57
- | --------------------------------------- | ------------------------------------------------------------ |
58
- | Add a "utils" file | Put it where it's used (shared module only at 3+ call sites) |
59
- | Create a base class | Use composition |
60
- | Add a config option nobody asked for | Don't |
61
- | Write a comment explaining _what_ | Rename so it's obvious |
62
- | Add error handling for impossible cases | Trust internal code |
63
- | Create a wrapper around a simple API | Use the API directly |
64
-
65
- ## 5 Rules of Programming
66
-
67
- 1. You can't tell where a program is going to spend its time. Bottlenecks occur in surprising places.
68
- 2. Measure. Don't tune for speed until you've measured.
69
- 3. Fancy algorithms are slow when n is small, and n is usually small.
70
- 4. Fancy algorithms are buggier than simple ones. Use simple algorithms and simple data structures.
71
- 5. Data dominates. If you've chosen the right data structures, the algorithms will be self-evident.